├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── README.md ├── fetch-rom-dsk.sh ├── fruitjam-build.sh ├── include ├── clocking.h ├── hw.h ├── kbd.h ├── tusb_config.h └── video.h ├── pico_sdk_import.cmake └── src ├── clocking.c ├── hid.c ├── kbd.c ├── main.c ├── pio_video.pio ├── sd_hw_config.c ├── video_hstx.c └── video_vga.c /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build pico-mac 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | check_suite: 9 | types: [rerequested] 10 | 11 | 12 | jobs: 13 | bins: 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Install ARM GCC 22 | uses: carlosperate/arm-none-eabi-gcc-action@v1 23 | with: 24 | release: '13.2.Rel1' 25 | 26 | - name: install sdl 27 | run: sudo apt-get update && sudo apt-get install -y eatmydata && sudo eatmydata apt-get install -y libsdl2-dev 28 | 29 | - name: get submodules 30 | run: git submodule update --init --recursive 31 | 32 | - name: get pico-sdk 33 | run: git clone --depth=1 -b adafruit-fruit-jam https://github.com/adafruit/pico-sdk ../pico-sdk 34 | 35 | - name: build targets 36 | run: | 37 | ./fetch-rom-dsk.sh 38 | ./fruitjam-build.sh -m 4096 39 | ./fruitjam-build.sh -m 4096 -v 40 | ./fruitjam-build.sh -m 400 41 | ./fruitjam-build.sh -m 400 -v 42 | ./fruitjam-build.sh -m 400 -o 43 | ./fruitjam-build.sh -m 400 -v -o 44 | mkdir uf2s 45 | cp build*/*.uf2 uf2s/ 46 | mkdir elfs 47 | cp build*/*.elf elfs/ 48 | 49 | - name: Upload artifact 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: uf2 files 53 | path: uf2s/* 54 | 55 | - name: Upload artifact 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: elf files 59 | path: elfs/* 60 | 61 | - name: Create release 62 | if: startsWith(github.ref, 'refs/tags/') 63 | uses: softprops/action-gh-release@v1 64 | with: 65 | files: build*/*.uf2 66 | fail_on_unmatched_files: true 67 | body: "Select a uf2 from the list below." 68 | 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.img 2 | *.bin 3 | os.7z 4 | picotool 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/umac"] 2 | url = https://github.com/adafruit/umac 3 | path = external/umac 4 | branch = soundemu 5 | [submodule "external/no-OS-FatFS-SD-SPI-RPi-Pico"] 6 | path = external/no-OS-FatFS-SD-SPI-RPi-Pico 7 | url = https://github.com/evansm7/no-OS-FatFS-SD-SPI-RPi-Pico.git 8 | [submodule "lib/tinyusb"] 9 | path = lib/tinyusb 10 | url = https://github.com/hathach/tinyusb 11 | [submodule "lib/Pico-PIO-USB"] 12 | path = lib/Pico-PIO-USB 13 | url = https://github.com/tannewt/Pico-PIO-USB 14 | [submodule "external/pico-extras"] 15 | path = external/pico-extras 16 | url = https://github.com/adafruit/pico-extras.git 17 | branch = i2s-audio-rp2350b-high-pins 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists 2 | # 3 | # MIT License 4 | # 5 | # Copyright (c) 2021, 2024 Matt Evans 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # 26 | cmake_minimum_required(VERSION 3.13) 27 | 28 | # Options that should be defined when initialising the build 29 | # directory with cmake, e.g. "cmake .. -DOPTION=true": 30 | # 31 | # Note: to build for pico2 / rp2350: cmake .. -DPICO_BOARD=pico2 32 | # Note: to build for fruit jam: 33 | # cmake -DBOARD=adafruit_fruit_jam -DPICO_BOARD=pico2 -DUSE_HSTX=1 -S . -B build_hstx -DPICO_SDK_PATH=../pico-sdk -DSD_TX=35 -DSD_RX=36 -DSD_SCK=34 -DSD_CS=39 34 | 35 | set(PICO_TINYUSB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib/tinyusb) 36 | set(PIOUSB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib/Pico-PIO-USB) 37 | 38 | set(UART_TX CACHE STRING "") 39 | set(UART_RX CACHE STRING "") 40 | set(UART CACHE STRING "") 41 | 42 | option(USE_SD "Build in SD support" OFF) 43 | set(SD_TX 3 CACHE STRING "SD SPI TX pin") 44 | set(SD_RX 4 CACHE STRING "SD SPI RX pin") 45 | set(SD_SCK 2 CACHE STRING "SD SPI SCK pin") 46 | set(SD_CS 5 CACHE STRING "SD SPI CS pin") 47 | set(SD_MHZ 5 CACHE STRING "SD SPI speed in MHz") 48 | 49 | option(USE_HSTX "Use HSTX digital video (only for rp2350 / pico2)" OFF) 50 | 51 | # Options for HSTX output (defaults are for Adafruit FruitJam) 52 | # HSTX always uses 640x480 for now 53 | set(HSTX_CKP 13 CACHE STRING "HSTX CK+ PIN") 54 | set(HSTX_D0P 15 CACHE STRING "HSTX D0+ PIN") 55 | set(HSTX_D1P 17 CACHE STRING "HSTX D1+ PIN") 56 | set(HSTX_D2P 19 CACHE STRING "HSTX D2+ PIN") 57 | 58 | option(OVERCLOCK "Overclock to 264MHz (known incompatible with psram)" OFF) 59 | 60 | # Options for analog VGA output 61 | option(USE_VGA_RES "Video uses VGA (640x480) resolution" OFF) 62 | set(VIDEO_PIN 18 CACHE STRING "VGA Video GPIO base pin (followed by VS, CLK, HS)") 63 | 64 | option(USE_PSRAM "Locate main Mac ram in PSRAM (only for rp2350 / pico 2)" OFF) 65 | set(PSRAM_CS 47 CACHE STRING "PSRAM Chip select pin") 66 | 67 | # Pins for PIO-based USB host 68 | set(PIN_USB_HOST_DP 1 CACHE STRING "USB D+ PIN") 69 | set(PIN_USB_HOST_DM 2 CACHE STRING "USB D- PIN") 70 | 71 | set(USE_AUDIO 1 CACHE STRING "Use audio") 72 | set(PIN_AUDIO_PWM 41 CACHE STRING "Pin for PWM audio") 73 | 74 | # See below, -DMEMSIZE= will configure umac's memory size, 75 | # overriding defaults. 76 | set(MEMSIZE 128 CACHE STRING "Memory size, in KB") 77 | 78 | set(DISC_IMAGE ${CMAKE_CURRENT_SOURCE_DIR}/umac0ro.img CACHE FILEPATH "Built-in disk image") 79 | 80 | if (USE_HSTX) 81 | add_compile_definitions(USE_VGA_RES=1) 82 | add_compile_definitions(HSTX_CKP=${HSTX_CKP} HSTX_D0P=${HSTX_D0P} HSTX_D1P=${HSTX_D1P} HSTX_D2P=${HSTX_D2P}) 83 | set(VIDEO_SRC src/video_hstx.c) 84 | else() 85 | add_compile_definitions(GPIO_VID_BASE=${VIDEO_PIN}) 86 | set(VIDEO_SRC src/video_vga.c) 87 | endif() 88 | 89 | if (OVERCLOCK) 90 | add_compile_definitions(OVERCLOCK=1) 91 | set(OPT_OC "-oc") 92 | else() 93 | set(OPT_OC "") 94 | endif() 95 | 96 | if (USE_VGA_RES) 97 | add_compile_definitions(USE_VGA_RES=1) 98 | add_compile_definitions(DISP_WIDTH=640) 99 | add_compile_definitions(DISP_HEIGHT=480) 100 | set(RES "640x480") 101 | set(RESFLAG "-v") 102 | else() 103 | add_compile_definitions(DISP_WIDTH=512) 104 | add_compile_definitions(DISP_HEIGHT=342) 105 | set(RES "512x342") 106 | set(RESFLAG "") 107 | endif() 108 | 109 | if (USE_PSRAM) 110 | add_compile_definitions(PIN_PSRAM_CS=${PSRAM_CS} USE_PSRAM=1) 111 | set(OPT_PSRAM "-psram") 112 | else() 113 | add_compile_definitions(USE_PSRAM=0) 114 | set(OPT_PSRAM "") 115 | endif() 116 | 117 | set(FIRMWARE "pico-mac-${PICO_BOARD}-${MEMSIZE}k-${RES}${OPT_PSRAM}${OPT_OC}") 118 | 119 | 120 | # initialize the SDK based on PICO_SDK_PATH 121 | # note: this must happen before project() 122 | include(pico_sdk_import.cmake) 123 | 124 | project(${FIRMWARE}) 125 | 126 | # initialize the Raspberry Pi Pico SDK 127 | pico_sdk_init() 128 | 129 | # For TUSB host stuff: 130 | set(FAMILY rp2040) 131 | set(BOARD raspberry_pi_pico) 132 | 133 | # umac subproject (and Musashi sub-subproject) 134 | set(UMAC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/external/umac) 135 | set(UMAC_MUSASHI_PATH ${UMAC_PATH}/external/Musashi) 136 | set(UMAC_INCLUDE_PATHS ${UMAC_PATH}/include ${UMAC_MUSASHI_PATH}) 137 | 138 | # This isn't very nice, but hey it's Sunday :p 139 | set(UMAC_SOURCES 140 | ${UMAC_PATH}/src/disc.c 141 | ${UMAC_PATH}/src/main.c 142 | ${UMAC_PATH}/src/rom.c 143 | ${UMAC_PATH}/src/scc.c 144 | ${UMAC_PATH}/src/via.c 145 | ${UMAC_MUSASHI_PATH}/m68kcpu.c 146 | ${UMAC_MUSASHI_PATH}/m68kdasm.c 147 | ${UMAC_MUSASHI_PATH}/m68kops.c 148 | ${UMAC_MUSASHI_PATH}/softfloat/softfloat.c 149 | ) 150 | 151 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ggdb -g3 -O3 -DPICO -DMUSASHI_CNF=\\\"../include/m68kconf.h\\\" -DUMAC_MEMSIZE=${MEMSIZE}") 152 | 153 | 154 | if (USE_SD) 155 | add_compile_definitions(USE_SD=1) 156 | set(FF_DISABLE_RTC ${PICO_RP2350}) # RP2350 doesn't have RTC, so disable it 157 | add_subdirectory(external/no-OS-FatFS-SD-SPI-RPi-Pico/FatFs_SPI build) 158 | set(EXTRA_SD_SRC src/sd_hw_config.c) 159 | set(EXTRA_SD_LIB FatFs_SPI) 160 | add_compile_definitions(SD_TX=${SD_TX} SD_RX=${SD_RX} SD_SCK=${SD_SCK} SD_CS=${SD_CS} SD_MHZ=${SD_MHZ}) 161 | endif() 162 | 163 | add_compile_definitions(PIN_USB_HOST_DP=${PIN_USB_HOST_DP}) 164 | add_compile_definitions(PIN_USB_HOST_DM=${PIN_USB_HOST_DM}) 165 | add_compile_definitions(PICO_DEFAULT_PIO_USB_DP_PIN=${PIN_USB_HOST_DP}) 166 | add_compile_definitions(PICO_DEFAULT_PIO_USB_DM_PIN=${PIN_USB_HOST_DM}) 167 | 168 | if (NOT UART STREQUAL "") 169 | add_compile_definitions(PICO_DEFAULT_UART=${UART}) 170 | endif() 171 | if (NOT UART_TX STREQUAL "") 172 | add_compile_definitions(PICO_DEFAULT_UART_TX_PIN=${UART_TX}) 173 | endif() 174 | if (NOT UART_RX STREQUAL "") 175 | add_compile_definitions(PICO_DEFAULT_UART_RX_PIN=${UART_RX}) 176 | endif() 177 | 178 | if (USE_AUDIO) 179 | add_subdirectory(external/pico-extras/src/rp2_common/pico_audio_i2s) 180 | add_subdirectory(external/pico-extras/src/common/pico_audio) 181 | add_subdirectory(external/pico-extras/src/common/pico_util_buffer) 182 | add_compile_definitions(ENABLE_AUDIO=1 PICO_AUDIO_I2S_CLOCK_PINS_SWAPPED=1 PICO_AUDIO_I2S_PIO=1 PICO_AUDIO_I2S_DMA_IRQ=0 PICO_AUDIO_I2S_DATA_PIN=24 PICO_AUDIO_I2S_CLOCK_PIN_BASE=25 PICO_AUDIO_I2S_MONO_INPUT=1 PICO_AUDIO_I2S_SWAP_CLOCK=1) 183 | set(EXTRA_AUDIO_LIB pico_util_buffer pico_audio pico_audio_i2s hardware_i2c) 184 | endif() 185 | 186 | if (TARGET tinyusb_device) 187 | add_executable(${FIRMWARE} 188 | src/main.c 189 | ${VIDEO_SRC} 190 | src/kbd.c 191 | src/hid.c 192 | src/clocking.c 193 | ${EXTRA_SD_SRC} 194 | 195 | ${PICO_TINYUSB_PATH}/src/portable/raspberrypi/pio_usb/hcd_pio_usb.c 196 | ${PIOUSB_PATH}/src/pio_usb.c 197 | ${PIOUSB_PATH}/src/pio_usb_host.c 198 | ${PIOUSB_PATH}/src/usb_crc.c 199 | 200 | ${UMAC_SOURCES} 201 | ) 202 | 203 | # The umac sources need to prepare Musashi (some sources are generated): 204 | add_custom_command(OUTPUT incbin/umac-rom.h 205 | COMMAND echo "*** Patching ROM ***" 206 | COMMAND set -xe && mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/incbin && make -C ${UMAC_PATH} patcher && ${UMAC_PATH}/patcher ${RESFLAG} -m ${MEMSIZE} -r "${CMAKE_CURRENT_LIST_DIR}/rom.bin" -w ${CMAKE_CURRENT_BINARY_DIR}/incbin/umac-rom.h 207 | ) 208 | add_custom_target(prepare_rom 209 | DEPENDS incbin/umac-rom.h 210 | ) 211 | 212 | add_custom_command(OUTPUT incbin/umac-disc.h 213 | COMMAND echo "DISC_IMAGE is ${DISC_IMAGE}" && mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/incbin && xxd -i < "${DISC_IMAGE}" > ${CMAKE_CURRENT_BINARY_DIR}/incbin/umac-disc.h 214 | ) 215 | add_custom_target(prepare_disc 216 | DEPENDS incbin/umac-disc.h 217 | ) 218 | 219 | add_custom_command(OUTPUT ${UMAC_MUSASHI_PATH}/m68kops.c 220 | COMMAND echo "*** Preparing umac source ***" 221 | COMMAND make -C ${UMAC_PATH} prepare 222 | ) 223 | add_custom_target(prepare_umac 224 | DEPENDS ${UMAC_MUSASHI_PATH}/m68kops.c 225 | ) 226 | add_dependencies(${FIRMWARE} prepare_umac prepare_rom prepare_disc) 227 | 228 | target_link_libraries(${FIRMWARE} 229 | pico_stdlib 230 | pico_multicore 231 | tinyusb_host 232 | tinyusb_board 233 | hardware_dma 234 | hardware_pio 235 | hardware_sync 236 | ${EXTRA_SD_LIB} 237 | ${EXTRA_AUDIO_LIB} 238 | ) 239 | 240 | target_include_directories(${FIRMWARE} PRIVATE 241 | ${CMAKE_CURRENT_LIST_DIR}/include 242 | ${PICO_TINYUSB_PATH}/hw 243 | ${PICO_TINYUSB_PATH}/src 244 | ${UMAC_INCLUDE_PATHS} 245 | ${PIOUSB_PATH}/src 246 | ${CMAKE_CURRENT_BINARY_DIR}/incbin 247 | ${CMAKE_CURRENT_LIST_DIR} 248 | ) 249 | 250 | if (NOT USE_HSTX) 251 | pico_generate_pio_header(${FIRMWARE} ${CMAKE_CURRENT_LIST_DIR}/src/pio_video.pio) 252 | endif() 253 | 254 | pico_enable_stdio_uart(${FIRMWARE} 1) 255 | 256 | # Needed for UF2: 257 | pico_add_extra_outputs(${FIRMWARE}) 258 | 259 | elseif(PICO_ON_DEVICE) 260 | message(WARNING "not building firmware because TinyUSB submodule is not initialized in the SDK") 261 | endif() 262 | 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pico Micro Mac (pico-umac) 2 | 3 | v0.21-fruitjam 28 March 2025 4 | 5 | I (@jepler) have run roughshod across the code, breaking things willy-nilly and adding 6 | 7 | * 512x342 & 640x480 digital output on HSTX 8 | * PIO USB 9 | * PSRAM support 10 | * Some Sound support on the onboard I2S DAC (speaker and headphones) 11 | 12 | Several pre-compiled variants are offered: 13 | * 400kB or 4096kB (the latter uses PSRAM, and may perform slower overall but can run more software) 14 | * 512x342 or 640x480 desktop resolution (512x342 is more compatible but has black screen margins) 15 | * overclocked or not (overclocked may run faster but may be less reliable) 16 | 17 | What works? 18 | * System beep 19 | * Dark Castle including audio 20 | * After Dark screensavers including audio 21 | * Glider works, but without sound 22 | 23 | What doesn't work? 24 | * Hypercard "play" and some hypercard screen transitions 25 | 26 | Some of the software I tested with: 27 | * https://archive.org/details/HyperCardBootSystem7 28 | * https://archive.org/details/mac\_DarkCastle\_1\_2 29 | * https://archive.org/details/AfterDark2 30 | 31 | Plug mouse & keyboard into the USB ports of the fruit jam. 32 | 33 | Put the software (a mac HFS volume with no additional headers or metadata) on a 34 | SD card as "umac0w.img" (if you want to be able to write files) or 35 | "umac0ro.img" (if you want the drive to be read only) and press the reset 36 | button to start. 37 | 38 | 39 | **Important note on overclocking:** The "oc" uf2 files overclock your RP2 chip to 264MHz. Simply including the `` header enables this overclocking, separate from the option in the Arduino Tools menu. 40 | Just like PC overclocking, there’s some risk of reduced component lifespan, though the extent (if any) can’t be precisely quantified and could vary from one chip to another. 41 | Proceed at your own discretion. 42 | 43 | v0.21 20 December 2024 44 | 45 | 46 | This project embeds the [umac Mac 128K 47 | emulator](https://github.com/evansm7/umac) project into a Raspberry Pi 48 | Pico microcontroller. At long last, the worst Macintosh in a cheap, 49 | portable form factor! 50 | 51 | It has features, many features, the best features: 52 | 53 | * Outputs VGA 640x480@60Hz, monochrome, using three resistors 54 | * USB HID keyboard and mouse 55 | * Read-only disc image in flash (your creations are ephemeral, like life itself) 56 | * Or, if you have a hard time letting go, support for rewritable 57 | disc storage on an SPI-attached SD card 58 | * Mac 128K by default, or you can make use of more of the Pico's 59 | memory and run as a _Mac 208K_ 60 | * Since you now have more memory, you can splash out on more 61 | screen real-estate, and use 640x480 resolution! 62 | 63 | Great features. It even doesn't hang at random! (Anymore.) 64 | 65 | The _Mac 208K_ was, of course, never a real machine. But, _umac_ 66 | supports odd-sized memories, and more memory runs more things. A 67 | surprising amount of software runs on the 128K config, but if you need 68 | to run _MacPaint_ specifically then you'll need to build both SD 69 | storage in addition to the _Mac 208K_ config. 70 | 71 | So anyway, you can build this project yourself for less than the cost 72 | of a beer! You'll need at least a RPi Pico board, a VGA monitor (or 73 | VGA-HDMI adapter), a USB mouse (and maybe a USB keyboard/hub), plus a 74 | couple of cheap components. 75 | 76 | # Build 77 | 78 | ## Prerequisites/essentials 79 | 80 | * git submodules 81 | - Clone the repo with `--recursive`, or `git submodule update --init --recursive` 82 | * Install/set up the [Pico/RP2040 SDK](https://github.com/raspberrypi/pico-sdk) 83 | * Get a ROM & disc image with `sh fetch-rom-dsk.sh` (needs curl & 7z (debian package p7zip-full)) 84 | 85 | ## Build pico-umac 86 | 87 | Run the configure-and-build script: 88 | ``` 89 | $ ./fruitjam-build.sh -h 90 | Usage: ./fruitjam-build.sh [-v] [-m KiB] [-d diskimage] 91 | 92 | -v: Use framebuffer resolution 640x480 instead of 512x342 93 | -m: Set memory size in KiB (over 400kB requires psram) 94 | -d: Specify disc image to include 95 | -o: Overclock to 264MHz (known to be incompatible with psram) 96 | 97 | PSRAM is automatically set depending on memory & framebuffer details 98 | ``` 99 | 100 | ## Disc image 101 | 102 | If you don't build SD support, an internal read-only disc image is 103 | stored in flash. If you do build SD support, you have the option to 104 | still include an image in flash, and this is used as a fallback if 105 | SD boot fails. 106 | 107 | Grab a Macintosh system disc from somewhere. A 400K or 800K floppy 108 | image works just fine, up to System 3.2 (the last version to support 109 | Mac128Ks). I've used images from 110 | but also check 111 | the various forums and MacintoshRepository. See the `umac` README for 112 | info on formats (it needs to be raw data without header). 113 | 114 | The image size can be whatever you have space for in flash (typically 115 | about 1.3MB is free there), or on the SD card. (I don't know what the 116 | HFS limits are. But if you make a 50MB disc you're unlikely to fill 117 | it with software that actually works on the _Mac 128K_ :) ) 118 | 119 | If using an SD card, use a FAT-formatted card and copy your disc image 120 | into _one_ of the following files in the root of the card: 121 | 122 | * `umac0.img`: A normal read/write disc image 123 | * `umac0ro.img`: A read-only disc image 124 | # Software 125 | 126 | Both CPU cores are used, and are optionally overclocked (blush) to 264MHz so that 127 | Missile Command is enjoyable to play. 128 | 129 | The `umac` emulator and video output runs on core 1, and core 0 deals 130 | with USB HID input. Video DMA is initialised pointing to the 131 | framebuffer in the Mac's RAM, or to a mirrored region in SRAM depending 132 | on the configuration. 133 | 134 | Other than that, it's just a main loop in `main.c` shuffling things 135 | into `umac`. 136 | 137 | Quite a lot of optimisation has been done in `umac` and `Musashi` to 138 | get performance up on Cortex-M0+ and the RP2040, like careful location 139 | of certain routines in RAM, ensuring inlining/constants can be 140 | foldeed, etc. It's 5x faster than it was at the beginning. 141 | 142 | The top-level project might be a useful framework for other emulators, 143 | or other projects that need USB HID input and a framebuffer (e.g. a 144 | VT220 emulator!). 145 | 146 | The USB HID code is largely stolen from the TinyUSB example, but shows 147 | how in practice you might capture keypresses/deal with mouse events. 148 | 149 | # Licence 150 | 151 | `hid.c` and `tusb_config.h` are based on code from the TinyUSB 152 | project, which is Copyright (c) 2019, 2021 Ha Thach (tinyusb.org) and 153 | released under the MIT licence. `sd_hw_config.c` is based on code 154 | from the no-OS-FatFS-SD-SPI-RPi-Pico project, which is Copyright (c) 155 | 2021 Carl John Kugler III. 156 | 157 | The remainder of the code is released under the MIT licence: 158 | 159 | Copyright (c) 2024 Matt Evans: 160 | 161 | Permission is hereby granted, free of charge, to any person obtaining a copy 162 | of this software and associated documentation files (the "Software"), to deal 163 | in the Software without restriction, including without limitation the rights 164 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 165 | copies of the Software, and to permit persons to whom the Software is 166 | furnished to do so, subject to the following conditions: 167 | 168 | The above copyright notice and this permission notice shall be included in all 169 | copies or substantial portions of the Software. 170 | 171 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 172 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 173 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 174 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 175 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 176 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 177 | SOFTWARE. 178 | 179 | -------------------------------------------------------------------------------- /fetch-rom-dsk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | 4 | if ! [ -f rom.bin ]; then 5 | curl -L 'https://ia902205.us.archive.org/view_archive.php?archive=/18/items/mac_rom_archive_-_as_of_8-19-2011/mac_rom_archive_-_as_of_8-19-2011.zip&file=4D1F8172%20-%20MacPlus%20v3.ROM' > rom.bin 6 | fi 7 | 8 | if ! [ -f umac0ro.img ]; then 9 | if ! [ -f os.7z ]; then 10 | curl -L 'https://archive.org/download/apple-mac-os-system-3.2-finder-5.3-system-tools-1.0-512-ke-jun-1986-3.5-800k.-7z/Apple%20Mac%20OS%20%28System%203.2%20Finder%205.3%29%20%28System%20Tools%201.0%20Mac%20128%2C%20512K%29%20%28Jun%201986%29%20%283.5-400k%29.7z' > 'os.7z' 11 | fi 12 | 7z x -so 'os.7z' 'Apple Mac OS (System 3.2 Finder 5.3) (System Tools 1.0 Mac 128, 512K) (Jun 1986) (3.5-400k)/System Installation.img' > umac0ro.img 13 | fi 14 | -------------------------------------------------------------------------------- /fruitjam-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Some configurations that actually work at the time I committed this: 5 | # ./fruitjam-build.sh -v # vga resolution, no psram, 128KiB 6 | # ./fruitjam-build.sh -v -m448 # vga resolution, no psram, 448KiB 7 | # ./fruitjam-build.sh -m4096 # 512x342 resolution, psram, 4096KiB 8 | # ./fruitjam-build.sh -d disk.img # specify disk image 9 | 10 | DISP_WIDTH=512 11 | DISP_HEIGHT=342 12 | MEMSIZE=400 13 | DISC_IMAGE= 14 | CMAKE_ARGS="" 15 | OVERCLOCK=0 16 | 17 | while getopts "hovd:m:" o; do 18 | case "$o" in 19 | (o) 20 | OVERCLOCK=1 21 | ;; 22 | (v) 23 | DISP_WIDTH=640 24 | DISP_HEIGHT=480 25 | CMAKE_ARGS="-DUSE_VGA_RES=1" 26 | ;; 27 | (m) 28 | MEMSIZE=$OPTARG 29 | ;; 30 | (d) 31 | DISC_IMAGE=$OPTARG 32 | ;; 33 | (h|?) 34 | echo "Usage: $0 [-v] [-m KiB] [-d diskimage]" 35 | echo "" 36 | echo " -v: Use framebuffer resolution 640x480 instead of 512x342" 37 | echo " -m: Set memory size in KiB (over 400kB requires psram)" 38 | echo " -d: Specify disc image to include" 39 | echo " -o: Overclock to 264MHz (known to be incompatible with psram)" 40 | echo "" 41 | echo "PSRAM is automatically set depending on memory & framebuffer details" 42 | exit 43 | ;; 44 | esac 45 | done 46 | 47 | shift $((OPTIND-1)) 48 | 49 | TAG=fruitjam_${DISP_WIDTH}x${DISP_HEIGHT}_${MEMSIZE}k 50 | PSRAM=$((MEMSIZE > 400)) 51 | if [ $PSRAM -ne 0 ] ; then 52 | if [ $OVERCLOCK -ne 0 ]; then 53 | echo "*** Overclock + PSRAM is known not to work. You have been warned." 54 | fi 55 | TAG=${TAG}_psram 56 | CMAKE_ARGS="$CMAKE_ARGS -DUSE_PSRAM=1" 57 | fi 58 | 59 | MIRROR_FRAMEBUFFER=$((USE_PSRAM || DISP_WIDTH != 640)) 60 | if [ "$MIRROR_FRAMEBUFFER" -eq 0 ]; then 61 | CMAKE_ARGS="$CMAKE_ARGS -DHSTX_CKP=12 -DHSTX_D0P=14 -DHSTX_D1P=16 -DHSTX_D2P=18 " 62 | fi 63 | 64 | # Append disk name to build directory if disk image is specified 65 | if [ -n "$DISC_IMAGE" ] && [ -f "$DISC_IMAGE" ]; then 66 | # Extract filename without extension 67 | CMAKE_ARGS="$CMAKE_ARGS -DDISC_IMAGE=${DISC_IMAGE}" 68 | DISC_IMAGE=$(basename "$DISC_IMAGE" | sed 's/\.[^.]*$//') 69 | TAG=${TAG}_${DISC_IMAGE} 70 | fi 71 | 72 | if [ $OVERCLOCK -ne 0 ]; then 73 | TAG=${TAG}_overclock 74 | fi 75 | 76 | set -x 77 | rm -rf build_${TAG} 78 | cmake -S . -B build_${TAG} \ 79 | -DPICO_SDK_PATH=../pico-sdk \ 80 | -DPICOTOOL_FETCH_FROM_GIT_PATH="$(pwd)/picotool" \ 81 | -DBOARD=adafruit_fruit_jam -DPICO_BOARD=adafruit_fruit_jam \ 82 | -DMEMSIZE=${MEMSIZE} \ 83 | -DUSE_HSTX=1 \ 84 | -DSD_TX=35 -DSD_RX=36 -DSD_SCK=34 -DSD_CS=39 -DUSE_SD=1 \ 85 | -DUART_TX=44 -DUART_RX=45 -DUART=0 \ 86 | -DBOARD_FILE=boards/adafruit_fruit_jam.c \ 87 | -DSD_MHZ=16 \ 88 | -DOVERCLOCK=${OVERCLOCK} \ 89 | ${CMAKE_ARGS} "$@" 90 | make -C build_${TAG} -j$(nproc) 91 | -------------------------------------------------------------------------------- /include/clocking.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | enum clk_sys_speed { 6 | CLK_SYS_264MHZ = 2, 7 | CLK_SYS_176MHZ = 3, 8 | CLK_SYS_132MHZ = 4, 9 | }; 10 | 11 | extern void overclock(enum clk_sys_speed clk_sys_div, uint32_t bit_clk_hz); 12 | -------------------------------------------------------------------------------- /include/hw.h: -------------------------------------------------------------------------------- 1 | /* 2 | * pico-umac pin definitions 3 | * 4 | * Copyright 2024 Matt Evans 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation files 8 | * (the "Software"), to deal in the Software without restriction, 9 | * including without limitation the rights to use, copy, modify, merge, 10 | * publish, distribute, sublicense, and/or sell copies of the Software, 11 | * and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #ifndef HW_H 28 | #define HW_H 29 | 30 | #define GPIO_LED_PIN PICO_DEFAULT_LED_PIN 31 | 32 | #define GPIO_VID_DATA GPIO_VID_BASE 33 | #define GPIO_VID_VS (GPIO_VID_DATA + 1) 34 | #define GPIO_VID_CLK (GPIO_VID_VS + 1) 35 | #define GPIO_VID_HS (GPIO_VID_CLK + 1) 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /include/kbd.h: -------------------------------------------------------------------------------- 1 | /* 2 | * pico-umac keyboard scancode mapping 3 | * 4 | * Copyright 2024 Matt Evans 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation files 8 | * (the "Software"), to deal in the Software without restriction, 9 | * including without limitation the rights to use, copy, modify, merge, 10 | * publish, distribute, sublicense, and/or sell copies of the Software, 11 | * and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | #ifndef KBD_H 28 | #define KBD_H 29 | 30 | #include 31 | #include 32 | 33 | bool kbd_queue_empty(); 34 | /* If empty, return 0, else return a mac keycode in [7:0] and [15] set if a press (else release) */ 35 | uint16_t kbd_queue_pop(); 36 | 37 | /* FIXME: map modifiers */ 38 | bool kbd_queue_push(uint8_t hid_keycode, bool pressed); 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /include/tusb_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | #ifndef _TUSB_CONFIG_H_ 27 | #define _TUSB_CONFIG_H_ 28 | 29 | #ifdef __cplusplus 30 | extern "C" { 31 | #endif 32 | 33 | //-------------------------------------------------------------------- 34 | // COMMON CONFIGURATION 35 | //-------------------------------------------------------------------- 36 | 37 | // defined by compiler flags for flexibility 38 | #ifndef CFG_TUSB_MCU 39 | #error CFG_TUSB_MCU must be defined 40 | #endif 41 | 42 | #if CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX 43 | #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_HOST | OPT_MODE_HIGH_SPEED) 44 | #else 45 | #define CFG_TUSB_RHPORT0_MODE OPT_MODE_HOST 46 | #endif 47 | 48 | #ifndef CFG_TUSB_OS 49 | #define CFG_TUSB_OS OPT_OS_NONE 50 | #endif 51 | 52 | // CFG_TUSB_DEBUG is defined by compiler in DEBUG build 53 | // #define CFG_TUSB_DEBUG 0 54 | 55 | /* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. 56 | * Tinyusb use follows macros to declare transferring memory so that they can be put 57 | * into those specific section. 58 | * e.g 59 | * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) 60 | * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) 61 | */ 62 | #ifndef CFG_TUSB_MEM_SECTION 63 | #define CFG_TUSB_MEM_SECTION 64 | #endif 65 | 66 | #ifndef CFG_TUSB_MEM_ALIGN 67 | #define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) 68 | #endif 69 | 70 | //-------------------------------------------------------------------- 71 | // CONFIGURATION 72 | //-------------------------------------------------------------------- 73 | 74 | // Size of buffer to hold descriptors and other data used for enumeration 75 | #define CFG_TUH_ENUMERATION_BUFSIZE 256 76 | 77 | #define CFG_TUH_HUB 1 78 | #define CFG_TUH_CDC 0 79 | #define CFG_TUH_HID 4 // typical keyboard + mouse device can have 3-4 HID interfaces 80 | #define CFG_TUH_MSC 0 81 | #define CFG_TUH_VENDOR 0 82 | 83 | // max device support (excluding hub device) 84 | #define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports 85 | 86 | #define CFG_TUH_RPI_PIO_USB 1 87 | 88 | //------------- HID -------------// 89 | #define CFG_TUH_HID_EPIN_BUFSIZE 64 90 | #define CFG_TUH_HID_EPOUT_BUFSIZE 64 91 | 92 | #ifndef BOARD_TUH_RHPORT 93 | #define BOARD_TUH_RHPORT 1 94 | #endif 95 | 96 | #ifdef __cplusplus 97 | } 98 | #endif 99 | 100 | #endif /* _TUSB_CONFIG_H_ */ 101 | -------------------------------------------------------------------------------- /include/video.h: -------------------------------------------------------------------------------- 1 | /* PIO video output 2 | * 3 | * Copyright 2024 Matt Evans 4 | * 5 | * Permission is hereby granted, free of charge, to any person 6 | * obtaining a copy of this software and associated documentation files 7 | * (the "Software"), to deal in the Software without restriction, 8 | * including without limitation the rights to use, copy, modify, merge, 9 | * publish, distribute, sublicense, and/or sell copies of the Software, 10 | * and to permit persons to whom the Software is furnished to do so, 11 | * subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | 26 | #ifndef VIDEO_H 27 | #define VIDEO_H 28 | 29 | #include 30 | 31 | void video_init(uint32_t *framebuffer); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /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 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 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 | FetchContent_Declare( 33 | pico_sdk 34 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 35 | GIT_TAG master 36 | ) 37 | if (NOT pico_sdk) 38 | message("Downloading Raspberry Pi Pico SDK") 39 | FetchContent_Populate(pico_sdk) 40 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 41 | endif () 42 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 43 | else () 44 | message(FATAL_ERROR 45 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 46 | ) 47 | endif () 48 | endif () 49 | 50 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 51 | if (NOT EXISTS ${PICO_SDK_PATH}) 52 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 53 | endif () 54 | 55 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 56 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 57 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 58 | endif () 59 | 60 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 61 | 62 | include(${PICO_SDK_INIT_CMAKE_FILE}) 63 | -------------------------------------------------------------------------------- /src/clocking.c: -------------------------------------------------------------------------------- 1 | #include "clocking.h" 2 | 3 | #include 4 | #include "pico.h" 5 | #include "pico/stdio.h" 6 | #include "hardware/clocks.h" 7 | #include "hardware/pll.h" 8 | #include "hardware/structs/ioqspi.h" 9 | #include "hardware/structs/qmi.h" 10 | #include "hardware/sync.h" 11 | #include "hardware/vreg.h" 12 | 13 | static void __no_inline_not_in_flash_func(set_qmi_timing)() { 14 | // Make sure flash is deselected - QMI doesn't appear to have a busy flag(!) 15 | while ((ioqspi_hw->io[1].status & IO_QSPI_GPIO_QSPI_SS_STATUS_OUTTOPAD_BITS) != IO_QSPI_GPIO_QSPI_SS_STATUS_OUTTOPAD_BITS) 16 | ; 17 | 18 | qmi_hw->m[0].timing = 0x40000202; 19 | //qmi_hw->m[0].timing = 0x40000101; 20 | // Force a read through XIP to ensure the timing is applied 21 | volatile uint32_t* ptr = (volatile uint32_t*)0x14000000; 22 | (void) *ptr; 23 | } 24 | 25 | #ifndef RP2350_PSRAM_MAX_SELECT_FS64 26 | #define RP2350_PSRAM_MAX_SELECT_FS64 (125000000) 27 | #endif 28 | 29 | #ifndef RP2350_PSRAM_MIN_DESELECT_FS 30 | #define RP2350_PSRAM_MIN_DESELECT_FS (50000000) 31 | #endif 32 | 33 | #ifndef RP2350_PSRAM_MAX_SCK_HZ 34 | #define RP2350_PSRAM_MAX_SCK_HZ (109000000) 35 | #endif 36 | 37 | #define SEC_TO_FS 1000000000000000ll 38 | 39 | static void __no_inline_not_in_flash_func(set_psram_timing)(void) { 40 | // Get secs / cycle for the system clock - get before disabling interrupts. 41 | uint32_t sysHz = (uint32_t)clock_get_hz(clk_sys); 42 | 43 | // Calculate the clock divider - goal to get clock used for PSRAM <= what 44 | // the PSRAM IC can handle - which is defined in RP2350_PSRAM_MAX_SCK_HZ 45 | volatile uint8_t clockDivider = (sysHz + RP2350_PSRAM_MAX_SCK_HZ - 1) / RP2350_PSRAM_MAX_SCK_HZ; 46 | 47 | uint32_t intr_stash = save_and_disable_interrupts(); 48 | 49 | // Get the clock femto seconds per cycle. 50 | 51 | uint32_t fsPerCycle = SEC_TO_FS / sysHz; 52 | 53 | // the maxSelect value is defined in units of 64 clock cycles 54 | // So maxFS / (64 * fsPerCycle) = maxSelect = RP2350_PSRAM_MAX_SELECT_FS64/fsPerCycle 55 | volatile uint8_t maxSelect = RP2350_PSRAM_MAX_SELECT_FS64 / fsPerCycle; 56 | 57 | // minDeselect time - in system clock cycle 58 | // Must be higher than 50ns (min deselect time for PSRAM) so add a fsPerCycle - 1 to round up 59 | // So minFS/fsPerCycle = minDeselect = RP2350_PSRAM_MIN_DESELECT_FS/fsPerCycle 60 | 61 | volatile uint8_t minDeselect = (RP2350_PSRAM_MIN_DESELECT_FS + fsPerCycle - 1) / fsPerCycle; 62 | 63 | printf("syshz=%u\n", sysHz); 64 | printf("Max Select: %d, Min Deselect: %d, clock divider: %d\n", maxSelect, minDeselect, clockDivider); 65 | 66 | qmi_hw->m[1].timing = QMI_M1_TIMING_PAGEBREAK_VALUE_1024 << QMI_M1_TIMING_PAGEBREAK_LSB | // Break between pages. 67 | 3 << QMI_M1_TIMING_SELECT_HOLD_LSB | // Delay releasing CS for 3 extra system cycles. 68 | 1 << QMI_M1_TIMING_COOLDOWN_LSB | 1 << QMI_M1_TIMING_RXDELAY_LSB | 69 | maxSelect << QMI_M1_TIMING_MAX_SELECT_LSB | minDeselect << QMI_M1_TIMING_MIN_DESELECT_LSB | 70 | clockDivider << QMI_M1_TIMING_CLKDIV_LSB; 71 | 72 | restore_interrupts(intr_stash); 73 | } 74 | 75 | 76 | static void __no_inline_not_in_flash_func(clock_init)(int sys_clk_div) { 77 | uint32_t intr_stash = save_and_disable_interrupts(); 78 | 79 | // Before messing with clock speeds ensure QSPI clock is nice and slow 80 | hw_write_masked(&qmi_hw->m[0].timing, 6, QMI_M0_TIMING_CLKDIV_BITS); 81 | 82 | // We're going to go fast, boost the voltage a little 83 | vreg_set_voltage(VREG_VOLTAGE_1_15); 84 | 85 | // Force a read through XIP to ensure the timing is applied before raising the clock rate 86 | volatile uint32_t* ptr = (volatile uint32_t*)0x14000000; 87 | (void) *ptr; 88 | 89 | // Before we touch PLLs, switch sys and ref cleanly away from their aux sources. 90 | hw_clear_bits(&clocks_hw->clk[clk_sys].ctrl, CLOCKS_CLK_SYS_CTRL_SRC_BITS); 91 | while (clocks_hw->clk[clk_sys].selected != 0x1) 92 | tight_loop_contents(); 93 | hw_write_masked(&clocks_hw->clk[clk_ref].ctrl, CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC, CLOCKS_CLK_REF_CTRL_SRC_BITS); 94 | while (clocks_hw->clk[clk_ref].selected != 0x4) 95 | tight_loop_contents(); 96 | 97 | // Stop the other clocks so we don't worry about overspeed 98 | clock_stop(clk_usb); 99 | clock_stop(clk_adc); 100 | clock_stop(clk_peri); 101 | clock_stop(clk_hstx); 102 | 103 | // Set USB PLL to 528MHz 104 | pll_init(pll_usb, PLL_COMMON_REFDIV, 1584 * MHZ, 3, 1); 105 | 106 | const uint32_t usb_pll_freq = 528 * MHZ; 107 | 108 | // CLK SYS = PLL USB 528MHz / sys_clk_div = 264MHz, 176MHz, or 132MHz 109 | clock_configure(clk_sys, 110 | CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, 111 | CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 112 | usb_pll_freq, usb_pll_freq / sys_clk_div); 113 | 114 | // CLK PERI = PLL USB 528MHz / 4 = 132MHz 115 | clock_configure(clk_peri, 116 | 0, // Only AUX mux on ADC 117 | CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 118 | usb_pll_freq, usb_pll_freq / 4); 119 | 120 | // CLK USB = PLL USB 528MHz / 11 = 48MHz 121 | clock_configure(clk_usb, 122 | 0, // No GLMUX 123 | CLOCKS_CLK_USB_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 124 | usb_pll_freq, 125 | USB_CLK_KHZ * KHZ); 126 | 127 | // CLK ADC = PLL USB 528MHz / 11 = 48MHz 128 | clock_configure(clk_adc, 129 | 0, // No GLMUX 130 | CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 131 | usb_pll_freq, 132 | USB_CLK_KHZ * KHZ); 133 | 134 | // Now we are running fast set fast QSPI clock and read delay 135 | set_qmi_timing(); 136 | 137 | restore_interrupts(intr_stash); 138 | } 139 | 140 | void overclock(enum clk_sys_speed clk_sys_div, uint32_t bit_clk_khz) { 141 | clock_init(clk_sys_div); 142 | stdio_init_all(); 143 | set_psram_timing(); 144 | #define SHOW_CLK(i) printf("clk_get_hz(%s) -> %u\n", #i, clock_get_hz(i)); 145 | SHOW_CLK(clk_gpout0); 146 | SHOW_CLK(clk_gpout1); 147 | SHOW_CLK(clk_gpout2); 148 | SHOW_CLK(clk_gpout3); 149 | SHOW_CLK(clk_ref); 150 | SHOW_CLK(clk_sys); 151 | SHOW_CLK(clk_peri); 152 | SHOW_CLK(clk_hstx); 153 | SHOW_CLK(clk_usb); 154 | SHOW_CLK(clk_adc); 155 | 156 | 157 | const uint32_t dvi_clock_khz = bit_clk_khz >> 1; 158 | printf("bit_clk_khz = %u dvi_clock_khz = %u\n", bit_clk_khz, dvi_clock_khz); 159 | uint vco_freq, post_div1, post_div2; 160 | if (!check_sys_clock_khz(dvi_clock_khz, &vco_freq, &post_div1, &post_div2)) 161 | panic("System clock of %u kHz cannot be exactly achieved", dvi_clock_khz); 162 | const uint32_t freq = vco_freq / (post_div1 * post_div2); 163 | 164 | // Set the sys PLL to the requested freq 165 | pll_init(pll_sys, PLL_COMMON_REFDIV, vco_freq, post_div1, post_div2); 166 | 167 | // CLK HSTX = Requested freq 168 | clock_configure(clk_hstx, 169 | 0, 170 | CLOCKS_CLK_HSTX_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 171 | freq, freq); 172 | } 173 | -------------------------------------------------------------------------------- /src/hid.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Derived from pico-examples/usb/host/host_cdc_msc_hid/hid_app.c, which is 3 | * Copyright (c) 2021, Ha Thach (tinyusb.org) 4 | * Further changes are Copyright 2024 Matt Evans 5 | * 6 | * The MIT License (MIT) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | #include "bsp/rp2040/boards/adafruit_fruit_jam/board.h" 28 | #include "tusb.h" 29 | 30 | #include "kbd.h" 31 | 32 | //--------------------------------------------------------------------+ 33 | // MACRO TYPEDEF CONSTANT ENUM DECLARATION 34 | //--------------------------------------------------------------------+ 35 | 36 | // If your host terminal support ansi escape code such as TeraTerm 37 | // it can be use to simulate mouse cursor movement within terminal 38 | #define USE_ANSI_ESCAPE 0 39 | 40 | #define MAX_REPORT 4 41 | 42 | static uint8_t const keycode2ascii[128][2] = { HID_KEYCODE_TO_ASCII }; 43 | 44 | // Each HID instance can has multiple reports 45 | static struct 46 | { 47 | uint8_t report_count; 48 | tuh_hid_report_info_t report_info[MAX_REPORT]; 49 | } hid_info[CFG_TUH_HID]; 50 | 51 | static void process_kbd_report(hid_keyboard_report_t const *report); 52 | static void process_mouse_report(hid_mouse_report_t const * report); 53 | static void process_generic_report(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); 54 | 55 | void hid_app_task(void) 56 | { 57 | // nothing to do 58 | } 59 | 60 | //--------------------------------------------------------------------+ 61 | // TinyUSB Callbacks 62 | //--------------------------------------------------------------------+ 63 | 64 | // Invoked when device with hid interface is mounted 65 | // Report descriptor is also available for use. tuh_hid_parse_report_descriptor() 66 | // can be used to parse common/simple enough descriptor. 67 | // Note: if report descriptor length > CFG_TUH_ENUMERATION_BUFSIZE, it will be skipped 68 | // therefore report_desc = NULL, desc_len = 0 69 | void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) 70 | { 71 | printf("HID device address = %d, instance = %d is mounted\r\n", dev_addr, instance); 72 | 73 | // Interface protocol (hid_interface_protocol_enum_t) 74 | const char* protocol_str[] = { "None", "Keyboard", "Mouse" }; 75 | uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); 76 | 77 | printf("HID Interface Protocol = %s\r\n", protocol_str[itf_protocol]); 78 | 79 | // By default host stack will use activate boot protocol on supported interface. 80 | // Therefore for this simple example, we only need to parse generic report descriptor (with built-in parser) 81 | if ( itf_protocol == HID_ITF_PROTOCOL_NONE ) 82 | { 83 | hid_info[instance].report_count = tuh_hid_parse_report_descriptor(hid_info[instance].report_info, MAX_REPORT, desc_report, desc_len); 84 | printf("HID has %u reports \r\n", hid_info[instance].report_count); 85 | } 86 | 87 | // request to receive report 88 | // tuh_hid_report_received_cb() will be invoked when report is available 89 | if ( !tuh_hid_receive_report(dev_addr, instance) ) 90 | { 91 | printf("Error: cannot request to receive report\r\n"); 92 | } 93 | } 94 | 95 | // Invoked when device with hid interface is un-mounted 96 | void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) 97 | { 98 | printf("HID device address = %d, instance = %d is unmounted\r\n", dev_addr, instance); 99 | } 100 | 101 | // Invoked when received report from device via interrupt endpoint 102 | void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) 103 | { 104 | uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); 105 | 106 | switch (itf_protocol) 107 | { 108 | case HID_ITF_PROTOCOL_KEYBOARD: 109 | TU_LOG2("HID receive boot keyboard report\r\n"); 110 | process_kbd_report( (hid_keyboard_report_t const*) report ); 111 | break; 112 | 113 | case HID_ITF_PROTOCOL_MOUSE: 114 | TU_LOG2("HID receive boot mouse report\r\n"); 115 | process_mouse_report( (hid_mouse_report_t const*) report ); 116 | break; 117 | 118 | default: 119 | // Generic report requires matching ReportID and contents with previous parsed report info 120 | process_generic_report(dev_addr, instance, report, len); 121 | break; 122 | } 123 | 124 | // continue to request to receive report 125 | if ( !tuh_hid_receive_report(dev_addr, instance) ) 126 | { 127 | printf("Error: cannot request to receive report\r\n"); 128 | } 129 | } 130 | 131 | //--------------------------------------------------------------------+ 132 | // Keyboard 133 | //--------------------------------------------------------------------+ 134 | 135 | static inline bool find_key_in_report(hid_keyboard_report_t const *report, uint8_t keycode) 136 | { 137 | for(uint8_t i=0; i<6; i++) 138 | { 139 | if (report->keycode[i] == keycode) return true; 140 | } 141 | 142 | return false; 143 | } 144 | 145 | static void process_kbd_report(hid_keyboard_report_t const *report) 146 | { 147 | /* Previous report is stored to compare against for key release: */ 148 | static hid_keyboard_report_t prev_report = { 0, 0, {0} }; 149 | 150 | for(uint8_t i=0; i<6; i++) { 151 | if (report->keycode[i]) { 152 | if (find_key_in_report(&prev_report, report->keycode[i])) { 153 | /* Key held */ 154 | } else { 155 | /* printf("Key pressed: %02x\n", report->keycode[i]); */ 156 | kbd_queue_push(report->keycode[i], true); 157 | } 158 | } 159 | if (prev_report.keycode[i] && !find_key_in_report(report, prev_report.keycode[i])) { 160 | /* printf("Key released: %02x\n", prev_report.keycode[i]); */ 161 | kbd_queue_push(prev_report.keycode[i], false); 162 | } 163 | } 164 | uint8_t mod_change = report->modifier ^ prev_report.modifier; 165 | if (mod_change) { 166 | uint8_t mp = mod_change & report->modifier; 167 | uint8_t mr = mod_change & prev_report.modifier; 168 | if (mp) { 169 | /* printf("Modifiers pressed %02x\n", mp); */ 170 | mp = (mp | (mp >> 4)) & 0xf; /* Don't care if left or right :P */ 171 | if (mp & 1) 172 | kbd_queue_push(HID_KEY_CONTROL_LEFT, true); 173 | if (mp & 2) 174 | kbd_queue_push(HID_KEY_SHIFT_LEFT, true); 175 | if (mp & 4) 176 | kbd_queue_push(HID_KEY_ALT_LEFT, true); 177 | if (mp & 8) 178 | kbd_queue_push(HID_KEY_GUI_LEFT, true); 179 | } 180 | if (mr) { 181 | /* printf("Modifiers released %02x\n", mr); */ 182 | mr = (mr | (mr >> 4)) & 0xf; 183 | if (mr & 1) 184 | kbd_queue_push(HID_KEY_CONTROL_LEFT, false); 185 | if (mr & 2) 186 | kbd_queue_push(HID_KEY_SHIFT_LEFT, false); 187 | if (mr & 4) 188 | kbd_queue_push(HID_KEY_ALT_LEFT, false); 189 | if (mr & 8) 190 | kbd_queue_push(HID_KEY_GUI_LEFT, false); 191 | } 192 | } 193 | prev_report = *report; 194 | } 195 | 196 | //--------------------------------------------------------------------+ 197 | // Mouse 198 | //--------------------------------------------------------------------+ 199 | 200 | /* Exported for use by other thread! */ 201 | int cursor_x = 0; 202 | int cursor_y = 0; 203 | int cursor_button = 0; 204 | 205 | #define MAX_DELTA 8 206 | 207 | static int clamp(int i) 208 | { 209 | return (i >= 0) ? (i > MAX_DELTA ? MAX_DELTA : i) : 210 | (i < -MAX_DELTA ? -MAX_DELTA : i); 211 | } 212 | 213 | static void process_mouse_report(hid_mouse_report_t const * report) 214 | { 215 | static hid_mouse_report_t prev_report = { 0 }; 216 | uint8_t button_changed_mask = report->buttons ^ prev_report.buttons; 217 | /* report->wheel can be used too... */ 218 | 219 | cursor_button = !!(report->buttons & MOUSE_BUTTON_LEFT); 220 | cursor_x += clamp(report->x); 221 | cursor_y += clamp(report->y); 222 | } 223 | 224 | //--------------------------------------------------------------------+ 225 | // Generic Report 226 | //--------------------------------------------------------------------+ 227 | static void process_generic_report(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) 228 | { 229 | (void) dev_addr; 230 | 231 | uint8_t const rpt_count = hid_info[instance].report_count; 232 | tuh_hid_report_info_t* rpt_info_arr = hid_info[instance].report_info; 233 | tuh_hid_report_info_t* rpt_info = NULL; 234 | 235 | if ( rpt_count == 1 && rpt_info_arr[0].report_id == 0) 236 | { 237 | // Simple report without report ID as 1st byte 238 | rpt_info = &rpt_info_arr[0]; 239 | }else 240 | { 241 | // Composite report, 1st byte is report ID, data starts from 2nd byte 242 | uint8_t const rpt_id = report[0]; 243 | 244 | // Find report id in the arrray 245 | for(uint8_t i=0; iusage_page == HID_USAGE_PAGE_DESKTOP ) 272 | { 273 | switch (rpt_info->usage) 274 | { 275 | case HID_USAGE_DESKTOP_KEYBOARD: 276 | TU_LOG1("HID receive keyboard report\r\n"); 277 | // Assume keyboard follow boot report layout 278 | process_kbd_report( (hid_keyboard_report_t const*) report ); 279 | break; 280 | 281 | case HID_USAGE_DESKTOP_MOUSE: 282 | TU_LOG1("HID receive mouse report\r\n"); 283 | // Assume mouse follow boot report layout 284 | process_mouse_report( (hid_mouse_report_t const*) report ); 285 | break; 286 | 287 | default: break; 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/kbd.c: -------------------------------------------------------------------------------- 1 | /* HID to Mac keyboard scancode mapping 2 | * 3 | * FIXME: This doesn't do capslock (needs to track toggle), and arrow 4 | * keys don't work. 5 | * 6 | * Copyright 2024 Matt Evans 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation files 10 | * (the "Software"), to deal in the Software without restriction, 11 | * including without limitation the rights to use, copy, modify, merge, 12 | * publish, distribute, sublicense, and/or sell copies of the Software, 13 | * and to permit persons to whom the Software is furnished to do so, 14 | * subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | */ 28 | 29 | #include 30 | #include "kbd.h" 31 | 32 | #include "class/hid/hid.h" 33 | #include "keymap.h" 34 | 35 | #define KQ_SIZE 32 36 | #define KQ_MASK (KQ_SIZE-1) 37 | 38 | static uint16_t kbd_queue[KQ_SIZE]; 39 | static unsigned int kbd_queue_prod = 0; 40 | static unsigned int kbd_queue_cons = 0; 41 | 42 | static bool kbd_queue_full() 43 | { 44 | return ((kbd_queue_prod + 1) & KQ_MASK) == kbd_queue_cons; 45 | } 46 | 47 | 48 | bool kbd_queue_empty() 49 | { 50 | return kbd_queue_prod == kbd_queue_cons; 51 | } 52 | 53 | /* If empty, return 0, else return a mac keycode in [7:0] and [15] set if a press (else release) */ 54 | uint16_t kbd_queue_pop() 55 | { 56 | if (kbd_queue_empty()) 57 | return 0; 58 | uint16_t v = kbd_queue[kbd_queue_cons]; 59 | kbd_queue_cons = (kbd_queue_cons + 1) & KQ_MASK; 60 | return v; 61 | } 62 | 63 | static const uint8_t hid_to_mac[256] = { 64 | [HID_KEY_NONE] = 0, 65 | [HID_KEY_A] = 255, // Hack for MKC_A, 66 | [HID_KEY_B] = MKC_B, 67 | [HID_KEY_C] = MKC_C, 68 | [HID_KEY_D] = MKC_D, 69 | [HID_KEY_E] = MKC_E, 70 | [HID_KEY_F] = MKC_F, 71 | [HID_KEY_G] = MKC_G, 72 | [HID_KEY_H] = MKC_H, 73 | [HID_KEY_I] = MKC_I, 74 | [HID_KEY_J] = MKC_J, 75 | [HID_KEY_K] = MKC_K, 76 | [HID_KEY_L] = MKC_L, 77 | [HID_KEY_M] = MKC_M, 78 | [HID_KEY_N] = MKC_N, 79 | [HID_KEY_O] = MKC_O, 80 | [HID_KEY_P] = MKC_P, 81 | [HID_KEY_Q] = MKC_Q, 82 | [HID_KEY_R] = MKC_R, 83 | [HID_KEY_S] = MKC_S, 84 | [HID_KEY_T] = MKC_T, 85 | [HID_KEY_U] = MKC_U, 86 | [HID_KEY_V] = MKC_V, 87 | [HID_KEY_W] = MKC_W, 88 | [HID_KEY_X] = MKC_X, 89 | [HID_KEY_Y] = MKC_Y, 90 | [HID_KEY_Z] = MKC_Z, 91 | [HID_KEY_1] = MKC_1, 92 | [HID_KEY_2] = MKC_2, 93 | [HID_KEY_3] = MKC_3, 94 | [HID_KEY_4] = MKC_4, 95 | [HID_KEY_5] = MKC_5, 96 | [HID_KEY_6] = MKC_6, 97 | [HID_KEY_7] = MKC_7, 98 | [HID_KEY_8] = MKC_8, 99 | [HID_KEY_9] = MKC_9, 100 | [HID_KEY_0] = MKC_0, 101 | [HID_KEY_ENTER] = MKC_Return, 102 | [HID_KEY_ESCAPE] = MKC_Escape, 103 | [HID_KEY_BACKSPACE] = MKC_BackSpace, 104 | [HID_KEY_TAB] = MKC_Tab, 105 | [HID_KEY_SPACE] = MKC_Space, 106 | [HID_KEY_MINUS] = MKC_Minus, 107 | [HID_KEY_EQUAL] = MKC_Equal, 108 | [HID_KEY_BRACKET_LEFT] = MKC_LeftBracket, 109 | [HID_KEY_BRACKET_RIGHT] = MKC_RightBracket, 110 | [HID_KEY_BACKSLASH] = MKC_BackSlash, 111 | [HID_KEY_SEMICOLON] = MKC_SemiColon, 112 | [HID_KEY_APOSTROPHE] = MKC_SingleQuote, 113 | [HID_KEY_GRAVE] = MKC_Grave, 114 | [HID_KEY_COMMA] = MKC_Comma, 115 | [HID_KEY_PERIOD] = MKC_Period, 116 | [HID_KEY_SLASH] = MKC_Slash, 117 | [HID_KEY_CAPS_LOCK] = MKC_CapsLock, 118 | [HID_KEY_F1] = MKC_F1, 119 | [HID_KEY_F2] = MKC_F2, 120 | [HID_KEY_F3] = MKC_F3, 121 | [HID_KEY_F4] = MKC_F4, 122 | [HID_KEY_F5] = MKC_F5, 123 | [HID_KEY_F6] = MKC_F6, 124 | [HID_KEY_F7] = MKC_F7, 125 | [HID_KEY_F8] = MKC_F8, 126 | [HID_KEY_F9] = MKC_F9, 127 | [HID_KEY_F10] = MKC_F10, 128 | [HID_KEY_F11] = MKC_F11, 129 | [HID_KEY_F12] = MKC_F12, 130 | [HID_KEY_PRINT_SCREEN] = MKC_Print, 131 | [HID_KEY_SCROLL_LOCK] = MKC_ScrollLock, 132 | [HID_KEY_PAUSE] = MKC_Pause, 133 | [HID_KEY_INSERT] = MKC_Help, 134 | [HID_KEY_HOME] = MKC_Home, 135 | [HID_KEY_PAGE_UP] = MKC_PageUp, 136 | [HID_KEY_DELETE] = MKC_BackSpace, 137 | [HID_KEY_END] = MKC_End, 138 | [HID_KEY_PAGE_DOWN] = MKC_PageDown, 139 | [HID_KEY_ARROW_RIGHT] = MKC_Right, 140 | [HID_KEY_ARROW_LEFT] = MKC_Left, 141 | [HID_KEY_ARROW_DOWN] = MKC_Down, 142 | [HID_KEY_ARROW_UP] = MKC_Up, 143 | /* [HID_KEY_NUM_LOCK] = MKC_, */ 144 | [HID_KEY_KEYPAD_DIVIDE] = MKC_KPDevide, 145 | [HID_KEY_KEYPAD_MULTIPLY] = MKC_KPMultiply, 146 | [HID_KEY_KEYPAD_SUBTRACT] = MKC_KPSubtract, 147 | [HID_KEY_KEYPAD_ADD] = MKC_KPAdd, 148 | [HID_KEY_KEYPAD_ENTER] = MKC_Enter, 149 | [HID_KEY_KEYPAD_1] = MKC_KP1, 150 | [HID_KEY_KEYPAD_2] = MKC_KP2, 151 | [HID_KEY_KEYPAD_3] = MKC_KP3, 152 | [HID_KEY_KEYPAD_4] = MKC_KP4, 153 | [HID_KEY_KEYPAD_5] = MKC_KP5, 154 | [HID_KEY_KEYPAD_6] = MKC_KP6, 155 | [HID_KEY_KEYPAD_7] = MKC_KP7, 156 | [HID_KEY_KEYPAD_8] = MKC_KP8, 157 | [HID_KEY_KEYPAD_9] = MKC_KP9, 158 | [HID_KEY_KEYPAD_0] = MKC_KP0, 159 | [HID_KEY_KEYPAD_DECIMAL] = MKC_Decimal, 160 | [HID_KEY_KEYPAD_EQUAL] = MKC_Equal, 161 | [HID_KEY_RETURN] = MKC_Return, 162 | /* [HID_KEY_POWER] = MKC_, */ 163 | /* [HID_KEY_KEYPAD_COMMA] = MKC_, */ 164 | /* [HID_KEY_KEYPAD_EQUAL_SIGN] = MKC_, */ 165 | [HID_KEY_CONTROL_LEFT] = MKC_Control, 166 | [HID_KEY_SHIFT_LEFT] = MKC_Shift, 167 | [HID_KEY_ALT_LEFT] = MKC_Option, 168 | [HID_KEY_GUI_LEFT] = MKC_Command, 169 | [HID_KEY_CONTROL_RIGHT] = MKC_Control, 170 | [HID_KEY_SHIFT_RIGHT] = MKC_Shift, 171 | [HID_KEY_ALT_RIGHT] = MKC_Option, 172 | [HID_KEY_GUI_RIGHT] = MKC_Command, 173 | }; 174 | 175 | static bool kbd_map(uint8_t hid_keycode, bool pressed, uint16_t *key_out) 176 | { 177 | uint8_t k = hid_to_mac[hid_keycode]; 178 | if (!k) 179 | return false; 180 | if (k == 255) 181 | k = MKC_A; // Hack, this is zero 182 | k = (k << 1) | 1; // FIXME just do this in the #defines 183 | *key_out = k | (pressed ? 0x8000 : 0); /* Convention w.r.t. main */ 184 | return true; 185 | } 186 | 187 | bool kbd_queue_push(uint8_t hid_keycode, bool pressed) 188 | { 189 | if (kbd_queue_full()) 190 | return false; 191 | 192 | uint16_t v; 193 | if (!kbd_map(hid_keycode, pressed, &v)) 194 | return false; 195 | 196 | kbd_queue[kbd_queue_prod] = v; 197 | kbd_queue_prod = (kbd_queue_prod + 1) & KQ_MASK; 198 | return true; 199 | } 200 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* pico-umac 2 | * 3 | * Main loop to initialise umac, and run main event loop (piping 4 | * keyboard/mouse events in). 5 | * 6 | * Copyright 2024 Matt Evans 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation files 10 | * (the "Software"), to deal in the Software without restriction, 11 | * including without limitation the rights to use, copy, modify, merge, 12 | * publish, distribute, sublicense, and/or sell copies of the Software, 13 | * and to permit persons to whom the Software is furnished to do so, 14 | * subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | */ 28 | 29 | #include 30 | #include 31 | #include 32 | #include "hardware/clocks.h" 33 | #include "hardware/gpio.h" 34 | #include "hardware/pio.h" 35 | #include "hardware/sync.h" 36 | #include "pico/multicore.h" 37 | #include "pico/stdlib.h" 38 | #include "pico/time.h" 39 | #include "hw.h" 40 | #include "video.h" 41 | #include "kbd.h" 42 | 43 | #include "pio_usb_configuration.h" 44 | #include "bsp/rp2040/boards/adafruit_fruit_jam/board.h" 45 | #include "tusb.h" 46 | 47 | #include "umac.h" 48 | #include "clocking.h" 49 | 50 | #if USE_SD 51 | #include "f_util.h" 52 | #include "ff.h" 53 | #include "rtc.h" 54 | #include "hw_config.h" 55 | #endif 56 | 57 | #if USE_PSRAM 58 | #include "hardware/structs/qmi.h" 59 | #include "hardware/structs/xip.h" 60 | #endif 61 | 62 | #if ENABLE_AUDIO 63 | #include "pico/audio_i2s.h" 64 | #include "hardware/i2c.h" 65 | uint8_t *audio_base; 66 | static void audio_setup(); 67 | static bool audio_poll(); 68 | static void set_mute_state(bool new_state); 69 | static absolute_time_t automute_time; 70 | #endif 71 | 72 | //////////////////////////////////////////////////////////////////////////////// 73 | // Imports and data 74 | 75 | extern void hid_app_task(void); 76 | extern int cursor_x; 77 | extern int cursor_y; 78 | extern int cursor_button; 79 | 80 | // Mac binary data: disc and ROM images 81 | static const uint8_t umac_disc[] = { 82 | #include "umac-disc.h" 83 | }; 84 | static const uint8_t umac_rom[] = { 85 | #include "umac-rom.h" 86 | }; 87 | 88 | #if USE_PSRAM 89 | #define umac_ram ((uint8_t*)0x11000000) 90 | #else 91 | static uint8_t umac_ram[RAM_SIZE]; 92 | #endif 93 | 94 | #define MIRROR_FRAMEBUFFER (USE_PSRAM || DISP_WIDTH != 640) 95 | #if MIRROR_FRAMEBUFFER 96 | static uint32_t umac_framebuffer_mirror[640*480/32]; 97 | #endif 98 | 99 | //////////////////////////////////////////////////////////////////////////////// 100 | 101 | static void io_init() 102 | { 103 | gpio_init(GPIO_LED_PIN); 104 | gpio_set_dir(GPIO_LED_PIN, GPIO_OUT); 105 | } 106 | 107 | static void poll_led_etc() 108 | { 109 | static absolute_time_t last = 0; 110 | absolute_time_t now = get_absolute_time(); 111 | 112 | if (absolute_time_diff_us(last, now) > 500*1000) { 113 | last = now; 114 | } 115 | } 116 | 117 | static int umac_cursor_x = 0; 118 | static int umac_cursor_y = 0; 119 | static int umac_cursor_button = 0; 120 | 121 | #define umac_get_audio_offset() (RAM_SIZE - 768) 122 | #if MIRROR_FRAMEBUFFER 123 | static void copy_framebuffer() { 124 | uint32_t *src = (uint32_t*)(umac_ram + umac_get_fb_offset()); 125 | #if DISP_WIDTH==640 && DISP_HEIGHT==480 126 | uint32_t *dest = umac_framebuffer_mirror; 127 | for(int i=0; i<640*480/32; i++) { 128 | *dest++ = *src++; 129 | } 130 | #elif DISP_WIDTH==512 && DISP_HEIGHT==342 131 | #define DISP_XOFFSET ((640 - DISP_WIDTH) / 32 / 2) 132 | #define DISP_YOFFSET ((480 - DISP_HEIGHT) / 2) 133 | #define LONGS_PER_INPUT_ROW (DISP_WIDTH / 32) 134 | #define LONGS_PER_OUTPUT_ROW (640 / 32) 135 | for(int i=0; i 16667; 158 | #if ENABLE_AUDIO 159 | if (automute_time < now) { 160 | automute_time = at_the_end_of_time; 161 | set_mute_state(false); 162 | } 163 | #endif 164 | #if ENABLE_AUDIO 165 | pending_vsync |= audio_poll(); 166 | #endif 167 | if (pending_vsync) { 168 | #if MIRROR_FRAMEBUFFER 169 | copy_framebuffer(); 170 | #endif 171 | /* FIXME: Trigger this off actual vsync */ 172 | umac_vsync_event(); 173 | last_vsync = now; 174 | } 175 | if (p_1hz >= 1000000) { 176 | umac_1hz_event(); 177 | last_1hz = now; 178 | } 179 | 180 | int update = 0; 181 | int dx = 0; 182 | int dy = 0; 183 | int b = umac_cursor_button; 184 | if (cursor_x != umac_cursor_x) { 185 | dx = cursor_x - umac_cursor_x; 186 | umac_cursor_x = cursor_x; 187 | update = 1; 188 | } 189 | if (cursor_y != umac_cursor_y) { 190 | dy = cursor_y - umac_cursor_y; 191 | umac_cursor_y = cursor_y; 192 | update = 1; 193 | } 194 | if (cursor_button != umac_cursor_button) { 195 | b = cursor_button; 196 | umac_cursor_button = cursor_button; 197 | update = 1; 198 | } 199 | if (update) { 200 | umac_mouse(dx, -dy, b); 201 | } 202 | 203 | if (!kbd_queue_empty()) { 204 | uint16_t k = kbd_queue_pop(); 205 | umac_kbd_event(k & 0xff, !!(k & 0x8000)); 206 | } 207 | } 208 | 209 | #if USE_SD 210 | static int disc_do_read(void *ctx, uint8_t *data, unsigned int offset, unsigned int len) 211 | { 212 | gpio_put(GPIO_LED_PIN, 1); 213 | FIL *fp = (FIL *)ctx; 214 | f_lseek(fp, offset); 215 | unsigned int did_read = 0; 216 | FRESULT fr = f_read(fp, data, len, &did_read); 217 | gpio_put(GPIO_LED_PIN, 0); 218 | if (fr != FR_OK || len != did_read) { 219 | printf("disc: f_read returned %d, read %u (of %u)\n", fr, did_read, len); 220 | return -1; 221 | } 222 | return 0; 223 | } 224 | 225 | static int disc_do_write(void *ctx, uint8_t *data, unsigned int offset, unsigned int len) 226 | { 227 | gpio_put(GPIO_LED_PIN, 1); 228 | FIL *fp = (FIL *)ctx; 229 | f_lseek(fp, offset); 230 | unsigned int did_write = 0; 231 | FRESULT fr = f_write(fp, data, len, &did_write); 232 | gpio_put(GPIO_LED_PIN, 0); 233 | if (fr != FR_OK || len != did_write) { 234 | printf("disc: f_write returned %d, read %u (of %u)\n", fr, did_write, len); 235 | return -1; 236 | } 237 | return 0; 238 | } 239 | 240 | static FIL discfp; 241 | #endif 242 | 243 | static void disc_setup(disc_descr_t discs[DISC_NUM_DRIVES]) 244 | { 245 | #if USE_SD 246 | char *disc0_name; 247 | const char *disc0_ro_name = "umac0ro.img"; 248 | const char *disc0_pattern = "umac0*.img"; 249 | 250 | /* Mount SD filesystem */ 251 | printf("Starting SPI/FatFS:\n"); 252 | set_spi_dma_irq_channel(true, false); 253 | sd_card_t *pSD = sd_get_by_num(0); 254 | FRESULT fr = f_mount(&pSD->fatfs, pSD->pcName, 1); 255 | printf(" mount: %d\n", fr); 256 | if (fr != FR_OK) { 257 | printf(" error mounting disc: %s (%d)\n", FRESULT_str(fr), fr); 258 | goto no_sd; 259 | } 260 | 261 | /* Look for a disc image */ 262 | DIR di = {0}; 263 | FILINFO fi = {0}; 264 | fr = f_findfirst(&di, &fi, "/", disc0_pattern); 265 | if (fr != FR_OK) { 266 | printf(" Can't find images %s: %s (%d)\n", disc0_pattern, FRESULT_str(fr), fr); 267 | goto no_sd; 268 | } 269 | disc0_name = fi.fname; 270 | f_closedir(&di); 271 | 272 | int read_only = !strcmp(disc0_name, disc0_ro_name); 273 | printf(" Opening %s (R%c)\n", disc0_name, read_only ? 'O' : 'W'); 274 | 275 | /* Open image, set up disc info: */ 276 | fr = f_open(&discfp, disc0_name, FA_OPEN_EXISTING | FA_READ | FA_WRITE); 277 | if (fr != FR_OK && fr != FR_EXIST) { 278 | printf(" *** Can't open %s: %s (%d)!\n", disc0_name, FRESULT_str(fr), fr); 279 | goto no_sd; 280 | } else { 281 | printf(" Opened, size %d (0x%x)\n", (unsigned)f_size(&discfp), (unsigned)f_size(&discfp)); 282 | if (read_only) 283 | printf(" (disc is read-only)\n"); 284 | discs[0].base = 0; // Means use R/W ops 285 | discs[0].read_only = read_only; 286 | discs[0].size = f_size(&discfp); 287 | discs[0].op_ctx = &discfp; 288 | discs[0].op_read = disc_do_read; 289 | discs[0].op_write = disc_do_write; 290 | } 291 | 292 | /* FIXME: Other files can be stored on SD too, such as logging 293 | * and NVRAM storage. 294 | * 295 | * We could also implement a menu here to select an image, 296 | * writing text to the framebuffer and checking kbd_queue_*() 297 | * for user input. 298 | */ 299 | return; 300 | 301 | no_sd: 302 | #endif 303 | /* If we don't find (or look for) an SD-based image, attempt 304 | * to use in-flash disc image: 305 | */ 306 | discs[0].base = (void *)umac_disc; 307 | discs[0].read_only = 1; 308 | discs[0].size = sizeof(umac_disc); 309 | } 310 | 311 | static void core1_main() 312 | { 313 | disc_descr_t discs[DISC_NUM_DRIVES] = {0}; 314 | 315 | printf("Core 1 started\n"); 316 | disc_setup(discs); 317 | 318 | umac_init(umac_ram, (void *)umac_rom, discs); 319 | /* Video runs on core 1, i.e. IRQs/DMA are unaffected by 320 | * core 0's USB activity. 321 | */ 322 | #if MIRROR_FRAMEBUFFER 323 | video_init((uint32_t *)(umac_framebuffer_mirror)); 324 | #else 325 | video_init((uint32_t *)(umac_ram + umac_get_fb_offset())); 326 | #endif 327 | 328 | #if ENABLE_AUDIO 329 | audio_base = (uint8_t*)umac_ram + umac_get_audio_offset(); 330 | #endif 331 | printf("Enjoyable Mac times now begin:\n\n"); 332 | 333 | while (true) { 334 | poll_umac(); 335 | } 336 | } 337 | 338 | size_t psram_size; 339 | 340 | size_t _psram_size; 341 | static void __no_inline_not_in_flash_func(setup_psram)(void) { 342 | _psram_size = 0; 343 | #if USE_PSRAM 344 | gpio_set_function(PIN_PSRAM_CS, GPIO_FUNC_XIP_CS1); 345 | uint32_t save = save_and_disable_interrupts(); 346 | // Try and read the PSRAM ID via direct_csr. 347 | qmi_hw->direct_csr = 30 << QMI_DIRECT_CSR_CLKDIV_LSB | 348 | QMI_DIRECT_CSR_EN_BITS; 349 | // Need to poll for the cooldown on the last XIP transfer to expire 350 | // (via direct-mode BUSY flag) before it is safe to perform the first 351 | // direct-mode operation 352 | while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) { 353 | } 354 | 355 | // Exit out of QMI in case we've inited already 356 | qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS; 357 | // Transmit as quad. 358 | qmi_hw->direct_tx = QMI_DIRECT_TX_OE_BITS | 359 | QMI_DIRECT_TX_IWIDTH_VALUE_Q << QMI_DIRECT_TX_IWIDTH_LSB | 360 | 0xf5; 361 | while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) { 362 | } 363 | (void)qmi_hw->direct_rx; 364 | qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS); 365 | 366 | // Read the id 367 | qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS; 368 | uint8_t kgd = 0; 369 | uint8_t eid = 0; 370 | for (size_t i = 0; i < 7; i++) { 371 | if (i == 0) { 372 | qmi_hw->direct_tx = 0x9f; 373 | } else { 374 | qmi_hw->direct_tx = 0xff; 375 | } 376 | while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_TXEMPTY_BITS) == 0) { 377 | } 378 | while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) { 379 | } 380 | if (i == 5) { 381 | kgd = qmi_hw->direct_rx; 382 | } else if (i == 6) { 383 | eid = qmi_hw->direct_rx; 384 | } else { 385 | (void)qmi_hw->direct_rx; 386 | } 387 | } 388 | // Disable direct csr. 389 | qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS | QMI_DIRECT_CSR_EN_BITS); 390 | 391 | if (kgd != 0x5D) { 392 | restore_interrupts(save); 393 | return; 394 | } 395 | 396 | // Enable quad mode. 397 | qmi_hw->direct_csr = 30 << QMI_DIRECT_CSR_CLKDIV_LSB | 398 | QMI_DIRECT_CSR_EN_BITS; 399 | // Need to poll for the cooldown on the last XIP transfer to expire 400 | // (via direct-mode BUSY flag) before it is safe to perform the first 401 | // direct-mode operation 402 | while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) { 403 | } 404 | 405 | // RESETEN, RESET and quad enable 406 | for (uint8_t i = 0; i < 3; i++) { 407 | qmi_hw->direct_csr |= QMI_DIRECT_CSR_ASSERT_CS1N_BITS; 408 | if (i == 0) { 409 | qmi_hw->direct_tx = 0x66; 410 | } else if (i == 1) { 411 | qmi_hw->direct_tx = 0x99; 412 | } else { 413 | qmi_hw->direct_tx = 0x35; 414 | } 415 | while ((qmi_hw->direct_csr & QMI_DIRECT_CSR_BUSY_BITS) != 0) { 416 | } 417 | qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS); 418 | for (size_t j = 0; j < 20; j++) { 419 | asm ("nop"); 420 | } 421 | (void)qmi_hw->direct_rx; 422 | } 423 | // Disable direct csr. 424 | qmi_hw->direct_csr &= ~(QMI_DIRECT_CSR_ASSERT_CS1N_BITS | QMI_DIRECT_CSR_EN_BITS); 425 | 426 | qmi_hw->m[1].timing = 427 | QMI_M0_TIMING_PAGEBREAK_VALUE_1024 << QMI_M0_TIMING_PAGEBREAK_LSB | // Break between pages. 428 | 3 << QMI_M0_TIMING_SELECT_HOLD_LSB | // Delay releasing CS for 3 extra system cycles. 429 | 1 << QMI_M0_TIMING_COOLDOWN_LSB | 430 | 1 << QMI_M0_TIMING_RXDELAY_LSB | 431 | 16 << QMI_M0_TIMING_MAX_SELECT_LSB | // In units of 64 system clock cycles. PSRAM says 8us max. 8 / 0.00752 / 64 = 16.62 432 | 7 << QMI_M0_TIMING_MIN_DESELECT_LSB | // In units of system clock cycles. PSRAM says 50ns.50 / 7.52 = 6.64 433 | 2 << QMI_M0_TIMING_CLKDIV_LSB; 434 | qmi_hw->m[1].rfmt = (QMI_M0_RFMT_PREFIX_WIDTH_VALUE_Q << QMI_M0_RFMT_PREFIX_WIDTH_LSB | 435 | QMI_M0_RFMT_ADDR_WIDTH_VALUE_Q << QMI_M0_RFMT_ADDR_WIDTH_LSB | 436 | QMI_M0_RFMT_SUFFIX_WIDTH_VALUE_Q << QMI_M0_RFMT_SUFFIX_WIDTH_LSB | 437 | QMI_M0_RFMT_DUMMY_WIDTH_VALUE_Q << QMI_M0_RFMT_DUMMY_WIDTH_LSB | 438 | QMI_M0_RFMT_DUMMY_LEN_VALUE_24 << QMI_M0_RFMT_DUMMY_LEN_LSB | 439 | QMI_M0_RFMT_DATA_WIDTH_VALUE_Q << QMI_M0_RFMT_DATA_WIDTH_LSB | 440 | QMI_M0_RFMT_PREFIX_LEN_VALUE_8 << QMI_M0_RFMT_PREFIX_LEN_LSB | 441 | QMI_M0_RFMT_SUFFIX_LEN_VALUE_NONE << QMI_M0_RFMT_SUFFIX_LEN_LSB); 442 | qmi_hw->m[1].rcmd = 0xeb << QMI_M0_RCMD_PREFIX_LSB | 443 | 0 << QMI_M0_RCMD_SUFFIX_LSB; 444 | qmi_hw->m[1].wfmt = (QMI_M0_WFMT_PREFIX_WIDTH_VALUE_Q << QMI_M0_WFMT_PREFIX_WIDTH_LSB | 445 | QMI_M0_WFMT_ADDR_WIDTH_VALUE_Q << QMI_M0_WFMT_ADDR_WIDTH_LSB | 446 | QMI_M0_WFMT_SUFFIX_WIDTH_VALUE_Q << QMI_M0_WFMT_SUFFIX_WIDTH_LSB | 447 | QMI_M0_WFMT_DUMMY_WIDTH_VALUE_Q << QMI_M0_WFMT_DUMMY_WIDTH_LSB | 448 | QMI_M0_WFMT_DUMMY_LEN_VALUE_NONE << QMI_M0_WFMT_DUMMY_LEN_LSB | 449 | QMI_M0_WFMT_DATA_WIDTH_VALUE_Q << QMI_M0_WFMT_DATA_WIDTH_LSB | 450 | QMI_M0_WFMT_PREFIX_LEN_VALUE_8 << QMI_M0_WFMT_PREFIX_LEN_LSB | 451 | QMI_M0_WFMT_SUFFIX_LEN_VALUE_NONE << QMI_M0_WFMT_SUFFIX_LEN_LSB); 452 | qmi_hw->m[1].wcmd = 0x38 << QMI_M0_WCMD_PREFIX_LSB | 453 | 0 << QMI_M0_WCMD_SUFFIX_LSB; 454 | 455 | restore_interrupts(save); 456 | 457 | _psram_size = 1024 * 1024; // 1 MiB 458 | uint8_t size_id = eid >> 5; 459 | if (eid == 0x26 || size_id == 2) { 460 | _psram_size *= 8; 461 | } else if (size_id == 0) { 462 | _psram_size *= 2; 463 | } else if (size_id == 1) { 464 | _psram_size *= 4; 465 | } 466 | 467 | // Mark that we can write to PSRAM. 468 | xip_ctrl_hw->ctrl |= XIP_CTRL_WRITABLE_M1_BITS; 469 | 470 | // Test write to the PSRAM. 471 | volatile uint32_t *psram_nocache = (volatile uint32_t *)0x15000000; 472 | psram_nocache[0] = 0x12345678; 473 | volatile uint32_t readback = psram_nocache[0]; 474 | if (readback != 0x12345678) { 475 | _psram_size = 0; 476 | return; 477 | } 478 | #endif 479 | } 480 | 481 | int main() 482 | { 483 | #if defined(OVERCLOCK) && OVERCLOCK+0 484 | overclock(CLK_SYS_264MHZ, 252000); 485 | #endif 486 | // set_sys_clock_khz(250*1000, true); 487 | 488 | setup_psram(); 489 | 490 | stdio_init_all(); 491 | io_init(); 492 | 493 | #define SHOW_CLK(i) printf("clk_get_hz(%s) -> %u\n", #i, clock_get_hz(i)); 494 | SHOW_CLK(clk_gpout0); 495 | SHOW_CLK(clk_gpout1); 496 | SHOW_CLK(clk_gpout2); 497 | SHOW_CLK(clk_gpout3); 498 | SHOW_CLK(clk_ref); 499 | SHOW_CLK(clk_sys); 500 | SHOW_CLK(clk_peri); 501 | SHOW_CLK(clk_hstx); 502 | SHOW_CLK(clk_usb); 503 | SHOW_CLK(clk_adc); 504 | 505 | #if ENABLE_AUDIO 506 | audio_setup(); 507 | #endif 508 | 509 | multicore_launch_core1(core1_main); 510 | 511 | printf("Starting, init usb\n"); 512 | 513 | pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG; 514 | pio_cfg.tx_ch = 2; 515 | pio_cfg.pin_dp = PICO_DEFAULT_PIO_USB_DP_PIN; 516 | _Static_assert(PIN_USB_HOST_DP + 1 == PIN_USB_HOST_DM || PIN_USB_HOST_DP - 1 == PIN_USB_HOST_DM, "Permitted USB D+/D- configuration"); 517 | pio_cfg.pinout = PIN_USB_HOST_DP + 1 == PIN_USB_HOST_DM ? PIO_USB_PINOUT_DPDM : PIO_USB_PINOUT_DMDP; 518 | 519 | #ifdef PICO_DEFAULT_PIO_USB_VBUSEN_PIN 520 | gpio_init(PICO_DEFAULT_PIO_USB_VBUSEN_PIN); 521 | gpio_set_dir(PICO_DEFAULT_PIO_USB_VBUSEN_PIN, GPIO_OUT); 522 | gpio_put(PICO_DEFAULT_PIO_USB_VBUSEN_PIN, PICO_DEFAULT_PIO_USB_VBUSEN_STATE); 523 | #endif 524 | 525 | tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); 526 | 527 | tuh_init(BOARD_TUH_RHPORT); 528 | 529 | /* This happens on core 0: */ 530 | while (true) { 531 | tuh_task(); 532 | hid_app_task(); 533 | poll_led_etc(); 534 | } 535 | 536 | return 0; 537 | } 538 | 539 | #if ENABLE_AUDIO 540 | 541 | #define I2C_ADDR 0x18 542 | 543 | void writeRegister(uint8_t reg, uint8_t value) { 544 | char buf[2]; 545 | buf[0] = reg; 546 | buf[1] = value; 547 | int res = i2c_write_timeout_us(i2c0, I2C_ADDR, buf, sizeof(buf), /* nostop */ false, 1000); 548 | if (res != 2) { 549 | printf("res=%d\n", res); 550 | panic("i2c_write_timeout failed: res=%d\n", res); 551 | } 552 | } 553 | 554 | uint8_t readRegister(uint8_t reg) { 555 | char buf[1]; 556 | buf[0] = reg; 557 | int res = i2c_write_timeout_us(i2c0, I2C_ADDR, buf, sizeof(buf), /* nostop */ true, 1000); 558 | if (res != 1) { 559 | panic("i2c_write_timeout failed: res=%d\n", res); 560 | } 561 | res = i2c_read_timeout_us(i2c0, I2C_ADDR, buf, sizeof(buf), /* nostop */ false, 1000); 562 | if (res != 1) { 563 | panic("i2c_read_timeout failed: res=%d\n", res); 564 | } 565 | uint8_t value = buf[0]; 566 | return value; 567 | } 568 | 569 | void modifyRegister(uint8_t reg, uint8_t mask, uint8_t value) { 570 | uint8_t current = readRegister(reg); 571 | uint8_t new_value = (current & ~mask) | (value & mask); 572 | writeRegister(reg, new_value); 573 | } 574 | 575 | void setPage(uint8_t page) { 576 | writeRegister(0x00, page); 577 | } 578 | 579 | 580 | void Wire_begin() { 581 | i2c_init(i2c0, 100000); 582 | gpio_set_function(20, GPIO_FUNC_I2C); 583 | gpio_set_function(21, GPIO_FUNC_I2C); 584 | } 585 | 586 | 587 | static void setup_i2s_dac() { 588 | gpio_init(22); 589 | gpio_set_dir(22, true); 590 | gpio_put(22, true); // allow i2s to come out of reset 591 | 592 | Wire_begin(); 593 | sleep_ms(1000); 594 | 595 | printf("initialize codec\n"); 596 | 597 | // Reset codec 598 | writeRegister(0x01, 0x01); 599 | sleep_ms(10); 600 | 601 | // Interface Control 602 | modifyRegister(0x1B, 0xC0, 0x00); 603 | modifyRegister(0x1B, 0x30, 0x00); 604 | 605 | // Clock MUX and PLL settings 606 | modifyRegister(0x04, 0x03, 0x03); 607 | modifyRegister(0x04, 0x0C, 0x04); 608 | 609 | writeRegister(0x06, 0x20); // PLL J 610 | writeRegister(0x08, 0x00); // PLL D LSB 611 | writeRegister(0x07, 0x00); // PLL D MSB 612 | 613 | modifyRegister(0x05, 0x0F, 0x02); // PLL P/R 614 | modifyRegister(0x05, 0x70, 0x10); 615 | 616 | // DAC/ADC Config 617 | modifyRegister(0x0B, 0x7F, 0x08); // NDAC 618 | modifyRegister(0x0B, 0x80, 0x80); 619 | 620 | modifyRegister(0x0C, 0x7F, 0x02); // MDAC 621 | modifyRegister(0x0C, 0x80, 0x80); 622 | 623 | modifyRegister(0x12, 0x7F, 0x08); // NADC 624 | modifyRegister(0x12, 0x80, 0x80); 625 | 626 | modifyRegister(0x13, 0x7F, 0x02); // MADC 627 | modifyRegister(0x13, 0x80, 0x80); 628 | 629 | // PLL Power Up 630 | modifyRegister(0x05, 0x80, 0x80); 631 | 632 | // Headset and GPIO Config 633 | setPage(1); 634 | modifyRegister(0x2e, 0xFF, 0x0b); 635 | setPage(0); 636 | modifyRegister(0x43, 0x80, 0x80); // Headset Detect 637 | modifyRegister(0x30, 0x80, 0x80); // INT1 Control 638 | modifyRegister(0x33, 0x3C, 0x14); // GPIO1 639 | 640 | 641 | // DAC Setup 642 | modifyRegister(0x3F, 0xC0, 0xC0); 643 | 644 | // DAC Routing 645 | setPage(1); 646 | modifyRegister(0x23, 0xC0, 0x40); 647 | modifyRegister(0x23, 0x0C, 0x04); 648 | 649 | // DAC Volume Control 650 | setPage(0); 651 | modifyRegister(0x40, 0x0C, 0x00); 652 | writeRegister(0x41, 0x0); // Left DAC Vol, 0dB 653 | writeRegister(0x42, 0x0); // Right DAC Vol, 0dB 654 | 655 | // Headphone and Speaker Setup 656 | setPage(1); 657 | modifyRegister(0x1F, 0xC0, 0xC0); // HP Driver Powered 658 | 659 | modifyRegister(0x28, 0x04, 0x04); // HP Left not muted 660 | modifyRegister(0x29, 0x04, 0x04); // HP Right not muted 661 | 662 | writeRegister(0x24, 50); // Left Analog HP, -26 dB 663 | writeRegister(0x25, 50); // Right Analog HP, -26 dB 664 | 665 | modifyRegister(0x28, 0x78, 0x00); // HP Left Gain, 0 db 666 | modifyRegister(0x29, 0x78, 0x00); // HP Right Gain, 0 db 667 | 668 | // Speaker Amp 669 | modifyRegister(0x20, 0x80, 0x80); // Amp enabled (0x80) disable with (0x00) 670 | modifyRegister(0x2A, 0x04, 0x04); // Not muted (0x04) mute with (0x00) 671 | modifyRegister(0x2A, 0x18, 0x08); // 0 dB gain 672 | writeRegister(0x26, 40); // amp gain, -20.1 dB 673 | 674 | // Return to page 0 675 | setPage(0); 676 | 677 | printf("Audio I2C Initialization complete!\n"); 678 | } 679 | static int volscale; 680 | 681 | 682 | #define SAMPLES_PER_BUFFER (370) 683 | int16_t audio[SAMPLES_PER_BUFFER]; 684 | 685 | void umac_audio_trap() { 686 | set_mute_state(volscale != 0); 687 | if(volscale) { 688 | automute_time = make_timeout_time_ms(500); 689 | } 690 | int32_t offset = 128; 691 | uint16_t *audiodata = (uint16_t*)audio_base; 692 | int scale = volscale; 693 | if (!scale) { 694 | memset(audio, 0, sizeof(audio)); 695 | return; 696 | } 697 | int16_t *stream = audio; 698 | for(int i=0; i> 8; 701 | *stream++ = a; 702 | } 703 | } 704 | 705 | struct audio_buffer_pool *producer_pool; 706 | 707 | static audio_format_t audio_format = { 708 | .format = AUDIO_BUFFER_FORMAT_PCM_S16, 709 | .sample_freq = 22256, // 60.15Hz*370, rounded up 710 | .channel_count = 1, 711 | }; 712 | 713 | const struct audio_i2s_config config = 714 | { 715 | .data_pin = PICO_AUDIO_I2S_DATA_PIN, 716 | .clock_pin_base = PICO_AUDIO_I2S_CLOCK_PIN_BASE, 717 | .pio_sm = 0, 718 | .dma_channel = 3 719 | }; 720 | 721 | static struct audio_buffer_format producer_format = { 722 | .format = &audio_format, 723 | .sample_stride = 2 724 | }; 725 | 726 | static void audio_setup() { 727 | setup_i2s_dac(); 728 | const struct audio_format *output_format = audio_i2s_setup(&audio_format, &config); 729 | assert(output_format); 730 | if (!output_format) { 731 | panic("PicoAudio: Unable to open audio device.\n"); 732 | } 733 | producer_pool = audio_new_producer_pool(&producer_format, 3, SAMPLES_PER_BUFFER); 734 | assert(producer_pool); 735 | bool ok = audio_i2s_connect(producer_pool); 736 | assert(ok); 737 | audio_i2s_set_enabled(true); 738 | } 739 | 740 | static bool audio_poll() { 741 | audio_buffer_t *buffer = take_audio_buffer(producer_pool, false); 742 | if (!buffer) return false; 743 | memcpy(buffer->buffer->bytes, audio, sizeof(audio)); 744 | buffer->sample_count = SAMPLES_PER_BUFFER; 745 | give_audio_buffer(producer_pool, buffer); 746 | return true; 747 | } 748 | 749 | static bool mute_state = false; 750 | static void set_mute_state(bool new_state) { 751 | if(mute_state == new_state) return; 752 | mute_state = new_state; 753 | 754 | setPage(1); 755 | if(mute_state) { 756 | modifyRegister(0x28, 0x04, 0x04); // HP Left not muted 757 | modifyRegister(0x29, 0x04, 0x04); // HP Right not muted 758 | modifyRegister(0x2A, 0x04, 0x04); // Speaker not muted 759 | } else { 760 | modifyRegister(0x28, 0x04, 0x0); // HP Left muted 761 | modifyRegister(0x29, 0x04, 0x0); // HP Right muted 762 | modifyRegister(0x2A, 0x04, 0x0); // Speaker muted 763 | } 764 | } 765 | 766 | void umac_audio_cfg(int volume, int sndres) { 767 | volscale = sndres ? 0 : 65536 * volume / 7; 768 | set_mute_state(volscale != 0); 769 | } 770 | #endif 771 | -------------------------------------------------------------------------------- /src/pio_video.pio: -------------------------------------------------------------------------------- 1 | ; PIO video output: 2 | ; This scans out video lines, characteristically some number of bits per pixel, 3 | ; a pixel clock, and timing signals HSync, VSync (in future, DE too). 4 | ; 5 | ; Copyright 2024 Matt Evans 6 | ; 7 | ; Permission is hereby granted, free of charge, to any person 8 | ; obtaining a copy of this software and associated documentation files 9 | ; (the "Software"), to deal in the Software without restriction, 10 | ; including without limitation the rights to use, copy, modify, merge, 11 | ; publish, distribute, sublicense, and/or sell copies of the Software, 12 | ; and to permit persons to whom the Software is furnished to do so, 13 | ; subject to the following conditions: 14 | ; 15 | ; The above copyright notice and this permission notice shall be 16 | ; included in all copies or substantial portions of the Software. 17 | ; 18 | ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | ; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | ; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | ; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 22 | ; BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 23 | ; ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | ; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | ; SOFTWARE. 26 | ; 27 | ; 28 | ; The source of image data is the OUT FIFO, and a corresponding C routine 29 | ; needs to fill this with image data. That data might be generated on 30 | ; the fly, or constructed by setting DMA pointers to a framebuffer. 31 | ; 32 | ; A typical usage would be the C routine preparing one scanline of data 33 | ; and setting off DMA. That's a good balance between number of interrupts 34 | ; and amount of buffering/RAM required (a framebuffer is generally pretty 35 | ; large...) 36 | ; 37 | ; Supports a max of 15bpp. That's tons given the number of IO... expecting 38 | ; to use this with 8, 3, etc. 39 | ; 40 | ; The output pins are required to be, in this order, 41 | ; 0: Video data 42 | ; 0+BPP: Vsync 43 | ; 1+BPP: PClk 44 | ; 2+BPP: Hsync 45 | ; 46 | ; FIXME: 1BPP for now 47 | ; 48 | ; The horizontal timing information is embedded in the data read via 49 | ; the FIFO, as follows, shown from the very start of a frame. The vertical 50 | ; timing info is generated entirely from the C side by passing a VSync 51 | ; value in the data stream. The data stream for each line is: 52 | ; 53 | ; ---------- Config information: (offset 0 on each line) ---------- 54 | ; 32b: Timing/sync info: 55 | ; [31] Vsync value for this line 56 | ; [30:23] Hsync width (HSW) 57 | ; [22:15] HBP width minus 3 (FIXME: check) 58 | ; [14:7] HFP width minus 3 59 | ; 32b: Number of visible pixels per line 60 | ; ---------- Pixel data: (offset 8 on each line) ------------------- 61 | ; : video data (padded with zeros for HBP/HFP pixels) 62 | ; ------------------------------------------------------------------- 63 | ; 64 | ; + +-------------------------------------------------- 65 | ; | |HBP- -HFP 66 | ; +-+ 67 | ; +--+ ***************************************************** 68 | ; | ***************************************************** 69 | ; +--+ ***************************************************** 70 | ; |VBP ***************************************************** 71 | ; | &&&&&&&+----------------------------------------+%%%% 72 | ; | &&&&&&&| |%%%% 73 | ; | &&&&&&&| Active area |%%%% 74 | ; | &&&&&&&| |%%%% 75 | ; | &&&&&&&| |%%%% 76 | ; | &&&&&&&| |%%%% 77 | ; | &&&&&&&| |%%%% 78 | ; | &&&&&&&| |%%%% 79 | ; | &&&&&&&| |%%%% 80 | ; | &&&&&&&| |%%%% 81 | ; | &&&&&&&| |%%%% 82 | ; | &&&&&&&| |%%%% 83 | ; | &&&&&&&| |%%%% 84 | ; | &&&&&&&| |%%%% 85 | ; | &&&&&&&+----------------------------------------+%%%% 86 | ; |VFP @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 87 | ; |: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 88 | ; |: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 89 | ; 90 | ; The HFP/HBP pixels should be written zero. Clever DMA programming can 91 | ; provide these from a separate location to the video data. 92 | ; 93 | ; FIXME: Add DE (and therefore full HBP/HFP counters in timing word) 94 | ; 95 | ; There are a couple of pin-mapping tricks going on. We need to be 96 | ; able to change the video without messing wtih VS, and we want to 97 | ; assert HS/VS at the same instant. That means the syncs aren't part 98 | ; of the OUT mapping -- this is only the pixel data. The SET mapping 99 | ; controls VS, and SIDESET controls HS/clk. 100 | ; 101 | ; The advantage of OUT being solely for data is then being able to easily 102 | ; extend to multiple BPP. Note the HS/VS are active-high in terms of 103 | ; programming, but the output signal can be flipped at GPIO using the 104 | ; inversion feature. 105 | 106 | ; .define BPP 123 etc. 107 | 108 | .program pio_video 109 | .side_set 2 ; SS[0] is clk, SS[1] is HS 110 | 111 | frame_start: 112 | ; The first word gives VS/HSW: [31]=vsync; [30:23]=HSW 113 | ; [22:15]=HBP, [14:7]=HFP. (shifted left!) 114 | ; 115 | ; Set VS on the same cycle as asserting HS. 116 | ; Note: these cycles are part of HFP. 117 | out X, 1 side 0 118 | jmp !X, vs_inactive side 1 119 | vs_active: 120 | set pins, 1 side 2 121 | jmp now_read_HSW side 3 122 | vs_inactive: 123 | set pins, 0 side 2 124 | nop side 3 125 | 126 | now_read_HSW: ; X=hsync width: 127 | out X, 8 side 2 128 | hsw_loop: nop side 3 129 | jmp X-- hsw_loop side 2 130 | 131 | ; De-assert hsync (leave Vsync as-is) and shift out HBP: 132 | now_read_HBP: ; X=HBP width: 133 | out X, 8 side 1 134 | hbp_loop: nop side 0 135 | jmp X-- hbp_loop side 1 136 | 137 | now_read_HFP: ; Y=HFP width: 138 | out Y, 8 side 0 139 | 140 | ; Pull, discarding the remainder of OSR. This prepares X pixel count. 141 | ; Note: these cycles (and HFP read) are part of HBP. 142 | pull block side 1 143 | out X, 32 side 0 144 | nop side 1 145 | pixels_loop: ; OSR primed/autopulled 146 | ; FIXME: side-set DE=1 147 | out pins, 1 side 0 ; BPP 148 | jmp X-- pixels_loop side 1 149 | ; FIXME: side-set DE=0 150 | ; Set video BLACK (1) 151 | mov pins, !NULL side 0 152 | nop side 1 153 | ; Now perform HFP delay 154 | hfp_loop: nop side 0 155 | jmp Y-- hfp_loop side 1 156 | 157 | ; A free HFP pixel, to prime for next line: 158 | // Auto-pull gets next line (always a multiple of 32b) 159 | nop side 0 160 | jmp frame_start side 1 161 | 162 | ; HFP 2 min 163 | ; HBP 2 min 164 | 165 | 166 | % c-sdk { 167 | static inline void pio_video_program_init(PIO pio, uint sm, uint offset, 168 | uint video_pin /* then VS, CLK, HS */, 169 | float clk_div) { 170 | /* Outputs are consecutive up from Video data */ 171 | uint vsync_pin = video_pin+1; 172 | uint clk_pin = video_pin+2; 173 | uint hsync_pin = video_pin+3; 174 | /* Init GPIO & directions */ 175 | pio_gpio_init(pio, video_pin); 176 | pio_gpio_init(pio, hsync_pin); 177 | pio_gpio_init(pio, vsync_pin); 178 | pio_gpio_init(pio, clk_pin); 179 | // FIXME: BPP define 180 | pio_sm_set_consecutive_pindirs(pio, sm, video_pin, 4, true /* out */); 181 | 182 | pio_sm_config c = pio_video_program_get_default_config(offset); 183 | sm_config_set_out_pins(&c, video_pin, 1); 184 | sm_config_set_set_pins(&c, vsync_pin, 1); 185 | sm_config_set_sideset_pins(&c, clk_pin); /* CLK + HS */ 186 | /* Sideset bits are configured via .side_set directive above */ 187 | sm_config_set_out_shift(&c, false /* OUT MSBs first */, true /* Autopull */, 32 /* bits */); 188 | sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); 189 | sm_config_set_clkdiv(&c, clk_div); 190 | 191 | pio_sm_init(pio, sm, offset, &c); 192 | pio_sm_set_enabled(pio, sm, true); 193 | } 194 | %} 195 | -------------------------------------------------------------------------------- /src/sd_hw_config.c: -------------------------------------------------------------------------------- 1 | /* hw_config.c 2 | Copyright 2021 Carl John Kugler III 3 | 4 | Licensed under the Apache License, Version 2.0 (the License); you may not use 5 | this file except in compliance with the License. You may obtain a copy of the 6 | License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an AS IS BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | */ 14 | /* 15 | 16 | This file should be tailored to match the hardware design. 17 | 18 | There should be one element of the spi[] array for each hardware SPI used. 19 | 20 | There should be one element of the sd_cards[] array for each SD card slot. 21 | The name is should correspond to the FatFs "logical drive" identifier. 22 | (See http://elm-chan.org/fsw/ff/doc/filename.html#vol) 23 | The rest of the constants will depend on the type of 24 | socket, which SPI it is driven by, and how it is wired. 25 | 26 | */ 27 | 28 | #include 29 | // 30 | #include "my_debug.h" 31 | // 32 | #include "hw_config.h" 33 | // 34 | #include "ff.h" /* Obtains integer types */ 35 | // 36 | #include "diskio.h" /* Declarations of disk functions */ 37 | 38 | // Hardware Configuration of SPI "objects" 39 | // Note: multiple SD cards can be driven by one SPI if they use different slave 40 | // selects. 41 | #ifndef SD_SPI_INST 42 | #define SD_SPI_INST (spi0) 43 | #endif 44 | 45 | static spi_t spis[] = { // One for each SPI. 46 | { 47 | .hw_inst = SD_SPI_INST, // SPI component 48 | .miso_gpio = SD_RX, // GPIO number (not pin number) 49 | .mosi_gpio = SD_TX, 50 | .sck_gpio = SD_SCK, 51 | 52 | .set_drive_strength = true, 53 | .mosi_gpio_drive_strength = GPIO_DRIVE_STRENGTH_8MA, 54 | .sck_gpio_drive_strength = GPIO_DRIVE_STRENGTH_8MA, 55 | 56 | // One of my cards doesn't seem to work beyond 5MHz :( 57 | .baud_rate = SD_MHZ * 1000 * 1000, 58 | } 59 | }; 60 | 61 | // Hardware Configuration of the SD Card "objects" 62 | static sd_card_t sd_cards[] = { // One for each SD card 63 | { 64 | .pcName = "0:", // Name used to mount device 65 | .spi = &spis[0], // Pointer to the SPI driving this card 66 | .ss_gpio = SD_CS, // The SPI slave select GPIO for this SD card 67 | .set_drive_strength = true, 68 | .ss_gpio_drive_strength = GPIO_DRIVE_STRENGTH_8MA, 69 | .use_card_detect = false, 70 | .card_detected_true = -1 // What the GPIO read returns when a card is 71 | // present. Use -1 if there is no card detect. 72 | }}; 73 | 74 | /* ********************************************************************** */ 75 | size_t sd_get_num() { return count_of(sd_cards); } 76 | sd_card_t *sd_get_by_num(size_t num) { 77 | if (num <= sd_get_num()) { 78 | return &sd_cards[num]; 79 | } else { 80 | return NULL; 81 | } 82 | } 83 | size_t spi_get_num() { return count_of(spis); } 84 | spi_t *spi_get_by_num(size_t num) { 85 | if (num <= sd_get_num()) { 86 | return &spis[num]; 87 | } else { 88 | return NULL; 89 | } 90 | } 91 | 92 | /* [] END OF FILE */ 93 | -------------------------------------------------------------------------------- /src/video_hstx.c: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2023 Scott Shawcroft for Adafruit Industries 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | #include "stdlib.h" 26 | 27 | // This is from: https://github.com/raspberrypi/pico-examples-rp2350/blob/a1/hstx/dvi_out_hstx_encoder/dvi_out_hstx_encoder.c 28 | 29 | #include "hardware/dma.h" 30 | #include "hardware/gpio.h" 31 | #include "hardware/structs/bus_ctrl.h" 32 | #include "hardware/structs/hstx_ctrl.h" 33 | #include "hardware/structs/hstx_fifo.h" 34 | 35 | // ---------------------------------------------------------------------------- 36 | // DVI constants 37 | 38 | #define TMDS_CTRL_00 0x354u 39 | #define TMDS_CTRL_01 0x0abu 40 | #define TMDS_CTRL_10 0x154u 41 | #define TMDS_CTRL_11 0x2abu 42 | 43 | #define SYNC_V0_H0 (TMDS_CTRL_00 | (TMDS_CTRL_00 << 10) | (TMDS_CTRL_00 << 20)) 44 | #define SYNC_V0_H1 (TMDS_CTRL_01 | (TMDS_CTRL_00 << 10) | (TMDS_CTRL_00 << 20)) 45 | #define SYNC_V1_H0 (TMDS_CTRL_10 | (TMDS_CTRL_00 << 10) | (TMDS_CTRL_00 << 20)) 46 | #define SYNC_V1_H1 (TMDS_CTRL_11 | (TMDS_CTRL_00 << 10) | (TMDS_CTRL_00 << 20)) 47 | 48 | #define MODE_H_SYNC_POLARITY 0 49 | #define MODE_H_FRONT_PORCH 16 50 | #define MODE_H_SYNC_WIDTH 96 51 | #define MODE_H_BACK_PORCH 48 52 | #define MODE_H_ACTIVE_PIXELS 640 53 | 54 | #define MODE_V_SYNC_POLARITY 0 55 | #define MODE_V_FRONT_PORCH 10 56 | #define MODE_V_SYNC_WIDTH 2 57 | #define MODE_V_BACK_PORCH 33 58 | #define MODE_V_ACTIVE_LINES 480 59 | 60 | #define MODE_H_TOTAL_PIXELS ( \ 61 | MODE_H_FRONT_PORCH + MODE_H_SYNC_WIDTH + \ 62 | MODE_H_BACK_PORCH + MODE_H_ACTIVE_PIXELS \ 63 | ) 64 | #define MODE_V_TOTAL_LINES ( \ 65 | MODE_V_FRONT_PORCH + MODE_V_SYNC_WIDTH + \ 66 | MODE_V_BACK_PORCH + MODE_V_ACTIVE_LINES \ 67 | ) 68 | 69 | #define HSTX_CMD_RAW (0x0u << 12) 70 | #define HSTX_CMD_RAW_REPEAT (0x1u << 12) 71 | #define HSTX_CMD_TMDS (0x2u << 12) 72 | #define HSTX_CMD_TMDS_REPEAT (0x3u << 12) 73 | #define HSTX_CMD_NOP (0xfu << 12) 74 | 75 | #define DO_BSWAP (1) 76 | #if DO_BSWAP 77 | #define BSWAP_MAYBE(x) (\ 78 | ((x) & 0xff) << 24 \ 79 | | (((x) >> 8) & 0xff) << 16 \ 80 | | (((x) >> 16) & 0xff) << 8 \ 81 | | (((x) >> 24) & 0xff) \ 82 | ) 83 | #else 84 | #define BSWAP_MAYBE(x) (x) 85 | #endif 86 | 87 | // ---------------------------------------------------------------------------- 88 | // HSTX command lists 89 | 90 | static uint32_t vblank_line_vsync_off[] = { 91 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_FRONT_PORCH), 92 | BSWAP_MAYBE(SYNC_V1_H1), 93 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_SYNC_WIDTH), 94 | BSWAP_MAYBE(SYNC_V1_H0), 95 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | (MODE_H_BACK_PORCH + MODE_H_ACTIVE_PIXELS)), 96 | BSWAP_MAYBE(SYNC_V1_H1), 97 | }; 98 | 99 | static uint32_t vblank_line_vsync_on[] = { 100 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_FRONT_PORCH), 101 | BSWAP_MAYBE(SYNC_V0_H1), 102 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_SYNC_WIDTH), 103 | BSWAP_MAYBE(SYNC_V0_H0), 104 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | (MODE_H_BACK_PORCH + MODE_H_ACTIVE_PIXELS)), 105 | BSWAP_MAYBE(SYNC_V0_H1), 106 | }; 107 | 108 | static uint32_t vactive_line[] = { 109 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_FRONT_PORCH), 110 | BSWAP_MAYBE(SYNC_V1_H1), 111 | BSWAP_MAYBE(HSTX_CMD_NOP), 112 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_SYNC_WIDTH), 113 | BSWAP_MAYBE(SYNC_V1_H0), 114 | BSWAP_MAYBE(HSTX_CMD_NOP), 115 | BSWAP_MAYBE(HSTX_CMD_RAW_REPEAT | MODE_H_BACK_PORCH), 116 | BSWAP_MAYBE(SYNC_V1_H1), 117 | BSWAP_MAYBE(HSTX_CMD_TMDS | MODE_H_ACTIVE_PIXELS), 118 | }; 119 | 120 | typedef struct { 121 | uint32_t *dma_commands; 122 | size_t dma_commands_len; // in words 123 | uint8_t dma_pixel_channel; 124 | uint8_t dma_command_channel; 125 | } picodvi_framebuffer_obj_t; 126 | 127 | picodvi_framebuffer_obj_t *active_picodvi = NULL; 128 | picodvi_framebuffer_obj_t picodvi; 129 | 130 | static void __not_in_flash_func(dma_irq_handler)(void) { 131 | if (active_picodvi == NULL) { 132 | return; 133 | } 134 | uint ch_num = active_picodvi->dma_pixel_channel; 135 | dma_channel_hw_t *ch = &dma_hw->ch[ch_num]; 136 | dma_hw->intr = 1u << ch_num; 137 | 138 | // Set the read_addr back to the start and trigger the first transfer (which 139 | // will trigger the pixel channel). 140 | ch = &dma_hw->ch[active_picodvi->dma_command_channel]; 141 | ch->al3_read_addr_trig = (uintptr_t)active_picodvi->dma_commands; 142 | } 143 | 144 | #define REAL_DISP_WIDTH 640 145 | #define REAL_DISP_HEIGHT 480 146 | 147 | void video_init(uint32_t *framebuffer) { 148 | picodvi_framebuffer_obj_t *self = &picodvi; 149 | 150 | // We compute all DMA transfers needed for a single frame. This ensure we don't have any super 151 | // quick interrupts that we need to respond to. Each transfer takes two words, trans_count and 152 | // read_addr. Active pixel lines need two transfers due to different read addresses. When pixel 153 | // doubling, then we must also set transfer size. 154 | size_t dma_command_size = 2; 155 | self->dma_commands_len = (MODE_V_FRONT_PORCH + MODE_V_SYNC_WIDTH + MODE_V_BACK_PORCH + 2 * MODE_V_ACTIVE_LINES + 1) * dma_command_size; 156 | self->dma_commands = (uint32_t *)malloc(self->dma_commands_len * sizeof(uint32_t)); 157 | if (self->dma_commands == NULL) { 158 | return; 159 | } 160 | 161 | self->dma_pixel_channel = dma_claim_unused_channel(true); 162 | self->dma_command_channel = dma_claim_unused_channel(true); 163 | 164 | size_t pixels_per_word = 32; 165 | size_t words_per_line = REAL_DISP_WIDTH / pixels_per_word; 166 | uint8_t rot = 24; // 24 + color_depth; 167 | size_t shift_amount = 31; // color_depth % 32; 168 | 169 | size_t command_word = 0; 170 | size_t frontporch_start = MODE_V_TOTAL_LINES - MODE_V_FRONT_PORCH; 171 | size_t frontporch_end = frontporch_start + MODE_V_FRONT_PORCH; 172 | size_t vsync_start = 0; 173 | size_t vsync_end = vsync_start + MODE_V_SYNC_WIDTH; 174 | size_t backporch_start = vsync_end; 175 | size_t backporch_end = backporch_start + MODE_V_BACK_PORCH; 176 | size_t active_start = backporch_end; 177 | 178 | uint32_t dma_ctrl = (self->dma_command_channel << DMA_CH0_CTRL_TRIG_CHAIN_TO_LSB) | 179 | (DREQ_HSTX << DMA_CH0_CTRL_TRIG_TREQ_SEL_LSB) | 180 | DMA_CH0_CTRL_TRIG_IRQ_QUIET_BITS | 181 | DMA_CH0_CTRL_TRIG_INCR_READ_BITS | 182 | DMA_CH0_CTRL_TRIG_EN_BITS; 183 | uint32_t dma_pixel_ctrl = dma_ctrl | ((pixels_per_word == 32 ? DMA_SIZE_32 : DMA_SIZE_16) << DMA_CH0_CTRL_TRIG_DATA_SIZE_LSB); 184 | #if DO_BSWAP 185 | dma_pixel_ctrl |= DMA_CH0_CTRL_TRIG_BSWAP_BITS; 186 | #endif 187 | dma_ctrl |= (DMA_SIZE_32 << DMA_CH0_CTRL_TRIG_DATA_SIZE_LSB); 188 | 189 | uint32_t dma_write_addr = (uint32_t)&hstx_fifo_hw->fifo; 190 | // Write ctrl and write_addr once when not pixel doubling because they don't 191 | // change. (write_addr doesn't change when pixel doubling either but we need 192 | // to rewrite it because it is after the ctrl register.) 193 | dma_channel_hw_addr(self->dma_pixel_channel)->al1_ctrl = dma_pixel_ctrl; 194 | dma_channel_hw_addr(self->dma_pixel_channel)->al1_write_addr = dma_write_addr; 195 | 196 | for (size_t v_scanline = 0; v_scanline < MODE_V_TOTAL_LINES; v_scanline++) { 197 | if (vsync_start <= v_scanline && v_scanline < vsync_end) { 198 | self->dma_commands[command_word++] = count_of(vblank_line_vsync_on); 199 | self->dma_commands[command_word++] = (uintptr_t)vblank_line_vsync_on; 200 | } else if (backporch_start <= v_scanline && v_scanline < backporch_end) { 201 | self->dma_commands[command_word++] = count_of(vblank_line_vsync_off); 202 | self->dma_commands[command_word++] = (uintptr_t)vblank_line_vsync_off; 203 | } else if (frontporch_start <= v_scanline && v_scanline < frontporch_end) { 204 | self->dma_commands[command_word++] = count_of(vblank_line_vsync_off); 205 | self->dma_commands[command_word++] = (uintptr_t)vblank_line_vsync_off; 206 | } else { 207 | self->dma_commands[command_word++] = count_of(vactive_line); 208 | self->dma_commands[command_word++] = (uintptr_t)vactive_line; 209 | size_t row = v_scanline - active_start; 210 | size_t transfer_count = words_per_line; 211 | self->dma_commands[command_word++] = transfer_count; 212 | uintptr_t row_start = row * (REAL_DISP_WIDTH / 8) + (uintptr_t)framebuffer; 213 | self->dma_commands[command_word++] = row_start; 214 | } 215 | } 216 | // Last command is NULL which will trigger an IRQ. 217 | self->dma_commands[command_word++] = 0; 218 | self->dma_commands[command_word++] = 0; 219 | 220 | // B&W 221 | size_t color_depth = 1; 222 | hstx_ctrl_hw->expand_tmds = 223 | (color_depth - 1) << HSTX_CTRL_EXPAND_TMDS_L2_NBITS_LSB | 224 | rot << HSTX_CTRL_EXPAND_TMDS_L2_ROT_LSB | 225 | (color_depth - 1) << HSTX_CTRL_EXPAND_TMDS_L1_NBITS_LSB | 226 | rot << HSTX_CTRL_EXPAND_TMDS_L1_ROT_LSB | 227 | (color_depth - 1) << HSTX_CTRL_EXPAND_TMDS_L0_NBITS_LSB | 228 | rot << HSTX_CTRL_EXPAND_TMDS_L0_ROT_LSB; 229 | size_t shifts_before_empty = (pixels_per_word % 32); 230 | 231 | // Pixels come in 32 bits at a time. color_depth dictates the number 232 | // of pixels per word. Control symbols (RAW) are an entire 32-bit word. 233 | hstx_ctrl_hw->expand_shift = 234 | (shifts_before_empty << HSTX_CTRL_EXPAND_SHIFT_ENC_N_SHIFTS_LSB) | 235 | (shift_amount << HSTX_CTRL_EXPAND_SHIFT_ENC_SHIFT_LSB) | 236 | 1 << HSTX_CTRL_EXPAND_SHIFT_RAW_N_SHIFTS_LSB | 237 | 0 << HSTX_CTRL_EXPAND_SHIFT_RAW_SHIFT_LSB; 238 | 239 | // Serial output config: clock period of 5 cycles, pop from command 240 | // expander every 5 cycles, shift the output shiftreg by 2 every cycle. 241 | hstx_ctrl_hw->csr = 0; 242 | hstx_ctrl_hw->csr = 243 | HSTX_CTRL_CSR_EXPAND_EN_BITS | 244 | (5u << HSTX_CTRL_CSR_CLKDIV_LSB) | 245 | (5u << HSTX_CTRL_CSR_N_SHIFTS_LSB) | 246 | (2u << HSTX_CTRL_CSR_SHIFT_LSB) | 247 | HSTX_CTRL_CSR_EN_BITS; 248 | 249 | // XXX this may be wrong, because pico-mac is using an overclock (but is it OC'ing HSTX? not sure) 250 | 251 | // Note we are leaving the HSTX clock at the SDK default of 125 MHz; since 252 | // we shift out two bits per HSTX clock cycle, this gives us an output of 253 | // 250 Mbps, which is very close to the bit clock for 480p 60Hz (252 MHz). 254 | // If we want the exact rate then we'll have to reconfigure PLLs. 255 | 256 | #define HSTX_FIRST_PIN 12 257 | 258 | // Setup the data to pin mapping. 259 | hstx_ctrl_hw->bit[(HSTX_CKP ) - HSTX_FIRST_PIN] = HSTX_CTRL_BIT0_CLK_BITS; 260 | hstx_ctrl_hw->bit[(HSTX_CKP ^ 1) - HSTX_FIRST_PIN] = HSTX_CTRL_BIT0_CLK_BITS | HSTX_CTRL_BIT0_INV_BITS; 261 | 262 | const int pinout[] = { HSTX_D0P, HSTX_D1P, HSTX_D2P }; 263 | 264 | for(uint lane = 0; lane < 3; lane++ ) { 265 | int bit = pinout[lane]; 266 | 267 | uint32_t lane_data_sel_bits = 268 | (lane * 10 ) << HSTX_CTRL_BIT0_SEL_P_LSB | 269 | (lane * 10 + 1) << HSTX_CTRL_BIT0_SEL_N_LSB; 270 | // The two halves of each pair get identical data, but one pin is inverted. 271 | hstx_ctrl_hw->bit[(bit ) - HSTX_FIRST_PIN] = lane_data_sel_bits; 272 | hstx_ctrl_hw->bit[(bit ^ 1) - HSTX_FIRST_PIN] = lane_data_sel_bits | HSTX_CTRL_BIT0_INV_BITS; 273 | } 274 | 275 | for (int i = 12; i <= 19; ++i) { 276 | gpio_set_function(i, 0); // HSTX 277 | } 278 | 279 | dma_channel_config c; 280 | c = dma_channel_get_default_config(self->dma_command_channel); 281 | channel_config_set_transfer_data_size(&c, DMA_SIZE_32); 282 | channel_config_set_read_increment(&c, true); 283 | channel_config_set_write_increment(&c, true); 284 | // This wraps the transfer back to the start of the write address. 285 | size_t wrap = 3; // 8 bytes because we write two DMA registers. 286 | volatile uint32_t *write_addr = &dma_hw->ch[self->dma_pixel_channel].al3_transfer_count; 287 | channel_config_set_ring(&c, true, wrap); 288 | // No chain because we use an interrupt to reload this channel instead of a 289 | // third channel. 290 | dma_channel_configure( 291 | self->dma_command_channel, 292 | &c, 293 | write_addr, 294 | self->dma_commands, 295 | (1 << wrap) / sizeof(uint32_t), 296 | false 297 | ); 298 | 299 | dma_hw->ints2 = (1u << self->dma_pixel_channel); 300 | dma_hw->inte2 = (1u << self->dma_pixel_channel); 301 | irq_set_exclusive_handler(DMA_IRQ_2, dma_irq_handler); 302 | irq_set_enabled(DMA_IRQ_2, true); 303 | 304 | bus_ctrl_hw->priority = BUSCTRL_BUS_PRIORITY_DMA_W_BITS | BUSCTRL_BUS_PRIORITY_DMA_R_BITS; 305 | 306 | active_picodvi = self; 307 | 308 | dma_irq_handler(); 309 | } 310 | -------------------------------------------------------------------------------- /src/video_vga.c: -------------------------------------------------------------------------------- 1 | /* Video output: 2 | * 3 | * Using PIO[1], output the Mac 512x342 1BPP framebuffer to VGA/pins. This is done 4 | * directly from the Mac framebuffer (without having to reformat in an intermediate 5 | * buffer). The video output is 640x480, with the visible pixel data centred with 6 | * borders: for analog VGA this is easy, as it just means increasing the horizontal 7 | * back porch/front porch (time between syncs and active video) and reducing the 8 | * display portion of a line. 9 | * 10 | * [1]: see pio_video.pio 11 | * 12 | * Copyright 2024 Matt Evans 13 | * 14 | * Permission is hereby granted, free of charge, to any person 15 | * obtaining a copy of this software and associated documentation files 16 | * (the "Software"), to deal in the Software without restriction, 17 | * including without limitation the rights to use, copy, modify, merge, 18 | * publish, distribute, sublicense, and/or sell copies of the Software, 19 | * and to permit persons to whom the Software is furnished to do so, 20 | * subject to the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 29 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 30 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 31 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | * SOFTWARE. 33 | */ 34 | 35 | #include 36 | #include 37 | #include 38 | #include "hardware/clocks.h" 39 | #include "hardware/dma.h" 40 | #include "hardware/gpio.h" 41 | #include "hardware/structs/padsbank0.h" 42 | #include "pio_video.pio.h" 43 | 44 | #include "hw.h" 45 | 46 | //////////////////////////////////////////////////////////////////////////////// 47 | /* VESA VGA mode 640x480@60 */ 48 | 49 | /* The pixel clock _should_ be (125/2/25.175) (about 2.483) but that seems to 50 | * make my VGA-HDMI adapter sample weird, and pixels crawl. Fudge a little, 51 | * looks better: 52 | */ 53 | #define VIDEO_PCLK_MULT (2.5*2) 54 | #define VIDEO_HSW 96 55 | #define VIDEO_HBP 48 56 | #define VIDEO_HRES 640 57 | #define VIDEO_HFP 16 58 | #define VIDEO_H_TOTAL_NOSYNC (VIDEO_HBP + VIDEO_HRES + VIDEO_HFP) 59 | #define VIDEO_VSW 2 60 | #define VIDEO_VBP 33 61 | #define VIDEO_VRES 480 62 | #define VIDEO_VFP 10 63 | #define VIDEO_V_TOTAL (VIDEO_VSW + VIDEO_VBP + VIDEO_VRES + VIDEO_VFP) 64 | /* The visible vertical span in the VGA output, [start, end) lines: */ 65 | #define VIDEO_V_VIS_START (VIDEO_VSW + VIDEO_VBP) 66 | #define VIDEO_V_VIS_END (VIDEO_V_VIS_START + VIDEO_VRES) 67 | 68 | #define VIDEO_FB_HRES DISP_WIDTH 69 | #define VIDEO_FB_VRES DISP_HEIGHT 70 | 71 | /* The lines at which the FB data is actively output: */ 72 | #define VIDEO_FB_V_VIS_START (VIDEO_V_VIS_START + ((VIDEO_VRES - VIDEO_FB_VRES)/2)) 73 | #define VIDEO_FB_V_VIS_END (VIDEO_FB_V_VIS_START + VIDEO_FB_VRES) 74 | 75 | /* Words of 1BPP pixel data per line; this dictates the length of the 76 | * video data DMA transfer: 77 | */ 78 | #define VIDEO_VISIBLE_WPL (VIDEO_FB_HRES / 32) 79 | 80 | #if (VIDEO_HRES & 31) 81 | #error "VIDEO_HRES: must be a multiple of 32b!" 82 | #endif 83 | 84 | //////////////////////////////////////////////////////////////////////////////// 85 | // Video DMA, framebuffer pointers 86 | 87 | static uint32_t video_null[VIDEO_VISIBLE_WPL]; 88 | static uint32_t *video_framebuffer; 89 | 90 | /* DMA buffer containing 2 pairs of per-line config words, for VS and not-VS: */ 91 | static uint32_t video_dma_cfg[4]; 92 | 93 | /* 3 DMA channels are used. The first to transfer data to PIO, and 94 | * the other two to transfer descriptors to the first channel. 95 | */ 96 | static uint8_t video_dmach_tx; 97 | static uint8_t video_dmach_descr_cfg; 98 | static uint8_t video_dmach_descr_data; 99 | 100 | typedef struct { 101 | const void *raddr; 102 | void *waddr; 103 | uint32_t count; 104 | uint32_t ctrl; 105 | } dma_descr_t; 106 | 107 | static dma_descr_t video_dmadescr_cfg; 108 | static dma_descr_t video_dmadescr_data; 109 | 110 | static volatile unsigned int video_current_y = 0; 111 | 112 | static int __not_in_flash_func(video_get_visible_y)(unsigned int y) { 113 | if ((y >= VIDEO_FB_V_VIS_START) && (y < VIDEO_FB_V_VIS_END)) { 114 | return y - VIDEO_FB_V_VIS_START; 115 | } else { 116 | return -1; 117 | } 118 | } 119 | 120 | static const uint32_t *__not_in_flash_func(video_line_addr)(unsigned int y) 121 | { 122 | int vy = video_get_visible_y(y); 123 | if (vy >= 0) 124 | return (const uint32_t *)&video_framebuffer[vy * VIDEO_VISIBLE_WPL]; 125 | else 126 | return (const uint32_t *)video_null; 127 | } 128 | 129 | static const uint32_t *__not_in_flash_func(video_cfg_addr)(unsigned int y) 130 | { 131 | return &video_dma_cfg[(y < VIDEO_VSW) ? 0 : 2]; 132 | } 133 | 134 | static void __not_in_flash_func(video_dma_prep_new)() 135 | { 136 | /* The descriptor DMA read pointers have moved on; reset them. 137 | * The write pointers wrap so should be pointing to the 138 | * correct DMA regs. 139 | */ 140 | dma_hw->ch[video_dmach_descr_cfg].read_addr = (uintptr_t)&video_dmadescr_cfg; 141 | dma_hw->ch[video_dmach_descr_cfg].transfer_count = 4; 142 | dma_hw->ch[video_dmach_descr_data].read_addr = (uintptr_t)&video_dmadescr_data; 143 | dma_hw->ch[video_dmach_descr_data].transfer_count = 4; 144 | 145 | /* Configure the two DMA descriptors, video_dmadescr_cfg and 146 | * video_dmadescr_data, to transfer from video config/data corresponding 147 | * to the current line. 148 | * 149 | * These descriptors will be used to program the video_dmach_tx channel, 150 | * pushing the buffer to PIO. 151 | * 152 | * This can be relatively relaxed, as it's triggered as line data 153 | * starts; we have until the end of the video line (when the descriptors 154 | * are retriggered) to program them. 155 | * 156 | * FIXME: this time could be used for something clever like split-screen 157 | * (e.g. info/text lines) constructed on-the-fly. 158 | */ 159 | video_dmadescr_cfg.raddr = video_cfg_addr(video_current_y); 160 | video_dmadescr_data.raddr = video_line_addr(video_current_y); 161 | 162 | /* Frame done */ 163 | if (++video_current_y >= VIDEO_V_TOTAL) 164 | video_current_y = 0; 165 | } 166 | 167 | static void __not_in_flash_func(video_dma_irq)() 168 | { 169 | /* The DMA IRQ occurs once the video portion of the line has been 170 | * triggered (not when the video transfer completes, but when the 171 | * descriptor transfer (that leads to the video transfer!) completes. 172 | * All we need to do is reconfigure the descriptors; the video DMA will 173 | * re-trigger the descriptors later. 174 | */ 175 | if (dma_channel_get_irq0_status(video_dmach_descr_data)) { 176 | dma_channel_acknowledge_irq0(video_dmach_descr_data); 177 | video_dma_prep_new(); 178 | } 179 | } 180 | 181 | static void video_prep_buffer() 182 | { 183 | memset(video_null, 0xff, VIDEO_VISIBLE_WPL * 4); 184 | 185 | unsigned int porch_padding = (VIDEO_HRES - VIDEO_FB_HRES)/2; 186 | // FIXME: HBP/HFP are prob off by one or so, check 187 | uint32_t timing = ((VIDEO_HSW - 1) << 23) | 188 | ((VIDEO_HBP + porch_padding - 3) << 15) | 189 | ((VIDEO_HFP + porch_padding - 4) << 7); 190 | video_dma_cfg[0] = timing | 0x80000000; 191 | video_dma_cfg[1] = VIDEO_FB_HRES - 1; 192 | video_dma_cfg[2] = timing; 193 | video_dma_cfg[3] = VIDEO_FB_HRES - 1; 194 | } 195 | 196 | static void video_init_dma() 197 | { 198 | /* pio_video expects each display line to be composed of two words of config 199 | * describing the line geometry and whether VS is asserted, followed by 200 | * visible data. 201 | * 202 | * To avoid having to embed config metadata in the display framebuffer, 203 | * we use two DMA transfers to PIO for each line. The first transfers 204 | * the config from a config buffer, and then triggers the second to 205 | * transfer the video data from the framebuffer. (This lets us use a 206 | * flat, regular FB.) 207 | * 208 | * The PIO side emits 1BPP MSB-first. The other advantage of 209 | * using a second DMA transfer is then we can also can 210 | * byteswap the DMA of the video portion to match the Mac 211 | * framebuffer layout. 212 | * 213 | * "Another caveat is that multiple channels should not be connected 214 | * to the same DREQ.": 215 | * The final complexity is that only one DMA channel can do the 216 | * transfers to PIO, because of how the credit-based flow control works. 217 | * So, _only_ channel 0 transfers from $SOME_BUFFER into the PIO FIFO, 218 | * and channel 1+2 are used to reprogram/trigger channel 0 from a DMA 219 | * descriptor list. 220 | * 221 | * Two extra channels are used to manage interrupts; ch1 programs ch0, 222 | * completes, and does nothing. (It programs a descriptor that causes 223 | * ch0 to transfer config, then trigger ch2 when complete.) ch2 then 224 | * programs ch0 with a descriptor to transfer data, then trigger ch1 225 | * when ch0 completes; when ch2 finishes doing that, it produces an IRQ. 226 | * Got that? 227 | * 228 | * The IRQ handler sets up ch1 and ch2 to point to 2 fresh cfg+data 229 | * descriptors; the deadline is by the end of ch0's data transfer 230 | * (i.e. a whole line). When ch0 finishes the data transfer it again 231 | * triggers ch1, and the new config entry is programmed. 232 | */ 233 | video_dmach_tx = dma_claim_unused_channel(true); 234 | video_dmach_descr_cfg = dma_claim_unused_channel(true); 235 | video_dmach_descr_data = dma_claim_unused_channel(true); 236 | 237 | /* Transmit DMA: config+video data */ 238 | /* First, make dmacfg for data to transfer from config buffers + data buffers: */ 239 | dma_channel_config dc_tx_c = dma_channel_get_default_config(video_dmach_tx); 240 | channel_config_set_dreq(&dc_tx_c, DREQ_PIO0_TX0); 241 | channel_config_set_transfer_data_size(&dc_tx_c, DMA_SIZE_32); 242 | channel_config_set_read_increment(&dc_tx_c, true); 243 | channel_config_set_write_increment(&dc_tx_c, false); 244 | channel_config_set_bswap(&dc_tx_c, false); 245 | /* Completion of the config TX triggers the video_dmach_descr_data channel */ 246 | channel_config_set_chain_to(&dc_tx_c, video_dmach_descr_data); 247 | video_dmadescr_cfg.raddr = NULL; /* Reprogrammed each line */ 248 | video_dmadescr_cfg.waddr = (void *)&pio0_hw->txf[0]; 249 | video_dmadescr_cfg.count = 2; /* 2 words of video config */ 250 | video_dmadescr_cfg.ctrl = dc_tx_c.ctrl; 251 | 252 | dma_channel_config dc_tx_d = dma_channel_get_default_config(video_dmach_tx); 253 | channel_config_set_dreq(&dc_tx_d, DREQ_PIO0_TX0); 254 | channel_config_set_transfer_data_size(&dc_tx_d, DMA_SIZE_32); 255 | channel_config_set_read_increment(&dc_tx_d, true); 256 | channel_config_set_write_increment(&dc_tx_d, false); 257 | channel_config_set_bswap(&dc_tx_d, true); /* This channel bswaps */ 258 | /* Completion of the data TX triggers the video_dmach_descr_cfg channel */ 259 | channel_config_set_chain_to(&dc_tx_d, video_dmach_descr_cfg); 260 | video_dmadescr_data.raddr = NULL; /* Reprogrammed each line */ 261 | video_dmadescr_data.waddr = (void *)&pio0_hw->txf[0]; 262 | video_dmadescr_data.count = VIDEO_VISIBLE_WPL; 263 | video_dmadescr_data.ctrl = dc_tx_d.ctrl; 264 | 265 | /* Now, the descr_cfg and descr_data channels transfer _those_ 266 | * descriptors to program the video_dmach_tx channel: 267 | */ 268 | dma_channel_config dcfg = dma_channel_get_default_config(video_dmach_descr_cfg); 269 | channel_config_set_transfer_data_size(&dcfg, DMA_SIZE_32); 270 | channel_config_set_read_increment(&dcfg, true); 271 | channel_config_set_write_increment(&dcfg, true); 272 | /* This channel loops on 16-byte/4-wprd boundary (i.e. writes all config): */ 273 | channel_config_set_ring(&dcfg, true, 4); 274 | /* No completion IRQ or chain: the video_dmach_tx DMA completes and triggers 275 | * the next 'data' descriptor transfer. 276 | */ 277 | dma_channel_configure(video_dmach_descr_cfg, &dcfg, 278 | &dma_hw->ch[video_dmach_tx].read_addr, 279 | &video_dmadescr_cfg, 280 | 4 /* 4 words of config */, 281 | false /* Not yet */); 282 | 283 | dma_channel_config ddata = dma_channel_get_default_config(video_dmach_descr_data); 284 | channel_config_set_transfer_data_size(&ddata, DMA_SIZE_32); 285 | channel_config_set_read_increment(&ddata, true); 286 | channel_config_set_write_increment(&ddata, true); 287 | channel_config_set_ring(&ddata, true, 4); 288 | /* This transfer has a completion IRQ. Receipt of that means that both 289 | * config and data descriptors have been transferred, and should be 290 | * reprogrammed for the next line. 291 | */ 292 | dma_channel_set_irq0_enabled(video_dmach_descr_data, true); 293 | dma_channel_configure(video_dmach_descr_data, &ddata, 294 | &dma_hw->ch[video_dmach_tx].read_addr, 295 | &video_dmadescr_data, 296 | 4 /* 4 words of config */, 297 | false /* Not yet */); 298 | 299 | /* Finally, set up video_dmadescr_cfg.raddr and video_dmadescr_data.raddr to point 300 | * to next line's video cfg/data buffers. Then, video_dmach_descr_cfg can be triggered 301 | * to start video. 302 | */ 303 | } 304 | 305 | //////////////////////////////////////////////////////////////////////////////// 306 | 307 | /* Initialise PIO, DMA, start sending pixels. Passed a pointer to a 512x342x1 308 | * Mac-order framebuffer. 309 | * 310 | * FIXME: Add an API to change the FB base after init live, e.g. for bank 311 | * switching. 312 | */ 313 | void video_init(uint32_t *framebuffer) 314 | { 315 | printf("Video init\n"); 316 | 317 | pio_video_program_init(pio0, 0, 318 | pio_add_program(pio0, &pio_video_program), 319 | GPIO_VID_DATA, /* Followed by HS, VS, CLK */ 320 | VIDEO_PCLK_MULT); 321 | 322 | /* Invert output pins: HS/VS are active-low, also invert video! */ 323 | gpio_set_outover(GPIO_VID_HS, GPIO_OVERRIDE_INVERT); 324 | gpio_set_outover(GPIO_VID_VS, GPIO_OVERRIDE_INVERT); 325 | gpio_set_outover(GPIO_VID_DATA, GPIO_OVERRIDE_INVERT); 326 | /* Highest drive strength (VGA is current-based, innit) */ 327 | hw_write_masked(&padsbank0_hw->io[GPIO_VID_DATA], 328 | PADS_BANK0_GPIO0_DRIVE_VALUE_12MA << PADS_BANK0_GPIO0_DRIVE_LSB, 329 | PADS_BANK0_GPIO0_DRIVE_BITS); 330 | 331 | /* IRQ handlers for DMA_IRQ_0: */ 332 | irq_set_exclusive_handler(DMA_IRQ_0, video_dma_irq); 333 | irq_set_enabled(DMA_IRQ_0, true); 334 | 335 | video_init_dma(); 336 | 337 | /* Init config word buffers */ 338 | video_current_y = 0; 339 | video_framebuffer = framebuffer; 340 | video_prep_buffer(); 341 | 342 | /* Set up pointers to first line, and start DMA */ 343 | video_dma_prep_new(); 344 | dma_channel_start(video_dmach_descr_cfg); 345 | } 346 | --------------------------------------------------------------------------------