From bd30ffe141fa0cafc1c5d4d38a0778e936456e54 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Tue, 7 May 2024 23:41:25 +0100 Subject: [PATCH] Initial release Squashed, from v0.1 --- .gitmodules | 3 + CMakeLists.txt | 110 ++++++++++++++ README.md | 234 ++++++++++++++++++++++++++++ external/umac | 1 + include/hw.h | 37 +++++ include/kbd.h | 40 +++++ include/tusb_config.h | 94 ++++++++++++ include/video.h | 33 ++++ pico_sdk_import.cmake | 62 ++++++++ src/hid.c | 290 +++++++++++++++++++++++++++++++++++ src/kbd.c | 199 ++++++++++++++++++++++++ src/main.c | 181 ++++++++++++++++++++++ src/pio_video.pio | 194 ++++++++++++++++++++++++ src/video.c | 344 ++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1822 insertions(+) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 160000 external/umac create mode 100644 include/hw.h create mode 100644 include/kbd.h create mode 100644 include/tusb_config.h create mode 100644 include/video.h create mode 100644 pico_sdk_import.cmake create mode 100644 src/hid.c create mode 100644 src/kbd.c create mode 100644 src/main.c create mode 100644 src/pio_video.pio create mode 100644 src/video.c diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f7fce88 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/umac"] + url = https://github.com/evansm7/umac + path = external/umac diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7371ab0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,110 @@ +# CMakeLists +# +# MIT License +# +# Copyright (c) 2021, 2024 Matt Evans +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# +cmake_minimum_required(VERSION 3.13) + +# initialize the SDK based on PICO_SDK_PATH +# note: this must happen before project() +include(pico_sdk_import.cmake) + +project(firmware) + +# initialize the Raspberry Pi Pico SDK +pico_sdk_init() + +# For TUSB host stuff: +set(FAMILY rp2040) +set(BOARD raspberry_pi_pico) + +set(TINYUSB_PATH ${PICO_SDK_PATH}/lib/tinyusb) + +# umac subproject (and Musashi sub-subproject) +set(UMAC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/external/umac) +set(UMAC_MUSASHI_PATH ${UMAC_PATH}/external/Musashi) +set(UMAC_INCLUDE_PATHS ${UMAC_PATH}/include ${UMAC_MUSASHI_PATH}) + +# This isn't very nice, but hey it's Sunday :p +set(UMAC_SOURCES + ${UMAC_PATH}/src/disc.c + ${UMAC_PATH}/src/main.c + ${UMAC_PATH}/src/rom.c + ${UMAC_PATH}/src/scc.c + ${UMAC_PATH}/src/via.c + ${UMAC_MUSASHI_PATH}/m68kcpu.c + ${UMAC_MUSASHI_PATH}/m68kdasm.c + ${UMAC_MUSASHI_PATH}/m68kops.c + ${UMAC_MUSASHI_PATH}/softfloat/softfloat.c + ) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -DPICO -DMUSASHI_CNF=\\\"../include/m68kconf.h\\\"") + +if (TARGET tinyusb_device) + add_executable(firmware + src/main.c + src/video.c + src/kbd.c + src/hid.c + + ${UMAC_SOURCES} + ) + + # The umac sources need to prepare Musashi (some sources are generated): + add_custom_command(OUTPUT ${UMAC_MUSASHI_PATH}/m68kops.c + COMMAND echo "*** Preparing umac source ***" + COMMAND make -C ${UMAC_PATH} prepare + ) + add_custom_target(prepare_umac + DEPENDS ${UMAC_MUSASHI_PATH}/m68kops.c + ) + add_dependencies(firmware prepare_umac) + + target_link_libraries(firmware + pico_stdlib + pico_multicore + tinyusb_host + tinyusb_board + hardware_dma + hardware_pio + hardware_sync + ) + + target_include_directories(firmware PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/include + ${TINYUSB_PATH}/hw + ${TINYUSB_PATH}/src + ${UMAC_INCLUDE_PATHS} + incbin + ) + + pico_generate_pio_header(firmware ${CMAKE_CURRENT_LIST_DIR}/src/pio_video.pio) + + pico_enable_stdio_uart(firmware 1) + + # Needed for UF2: + pico_add_extra_outputs(firmware) + +elseif(PICO_ON_DEVICE) + message(WARNING "not building firmware because TinyUSB submodule is not initialized in the SDK") +endif() + diff --git a/README.md b/README.md new file mode 100644 index 0000000..569a6b8 --- /dev/null +++ b/README.md @@ -0,0 +1,234 @@ +# Pico Micro Mac (pico-umac) + +v0.1 15 June 2024 + + +This project embeds the [umac Mac 128K +emulator](https://github.com/evansm7/umac) project into a Raspberry Pi +Pico microcontroller. At long last, the worst Macintosh in a cheap, +portable form factor! + +It has features, many features, the best features: + + * Outputs VGA 640x480@60Hz, monochrome, using three resistors + * USB HID keyboard and mouse + * Read-only disc image in flash (your creations are ephemeral, like life itself) + +Great features. It even doesn't hang at random! (Anymore.) + +So anyway, you can build this project yourself for less than the cost +of a beer! You'll need at least a RPi Pico board, a VGA monitor (or +VGA-HDMI adapter), a USB mouse (and maybe a USB keyboard/hub), plus a +couple of cheap components. + +# Software prerequisites + +Install and build `umac` first. It'll give you a preview of the fun +to come, plus is required to generate a patched ROM image. + +## Build essentials + + * git submodules + * Clone the repo with `--recursive`, or `git submodule update --init --recursive` + * Install/set up the [Pico/RP2040 SDK](https://github.com/raspberrypi/pico-sdk) + +Do the initial Pico SDK `cmake` setup into an out-of-tree build dir: + +``` +mkdir build +(cd build ; PICO_SDK_PATH=/path/to/sdk cmake ..) +``` + +## ROM image + +The flow is to use `umac` installed on your workstation (e.g. Linux, +but WSL may work too) to prepare a patched ROM image. + +`umac` is passed the 4D1F8172 MacPlusv3 ROM, and `-W` to write the +post-patching binary out: + +``` +~/code/umac$ ./main -r '4D1F8172 - MacPlus v3.ROM' -W rom.bin +``` + +## Disc image + +Grab a Macintosh system disc from somewhere. A 400K or 800K floppy +image works just fine, up to System 3.2 (the last version to support +Mac128Ks). I've used images from + but also check +the various forums and MacintoshRepository. See the `umac` README for +info on formats (it needs to be raw data without header). + +Let's call this `disc.bin`. + +## Putting it together, and building + +Given the `rom.bin` and `disc.bin` prepared above, you can now +generate includes from them and perform the build: + +``` +mkdir incbin +xxd -i < rom.bin > incbin/umac-rom.h +xxd -i < disc.bin > incbin/umac-disc.h +make -C build +``` + +You'll get a `build/firmware.uf2` out the other end. Flash this to +your Pico: e.g. plug it in with button held/drag/drop. (When +iterating/testing during development, unplugging the OTG cable each +time is a pain – I ended up moving to SWD probe programming.) + +The LED should flash at about 2Hz once powered up. + +# Hardware contruction + +It's a simple circuit in terms of having few components: just the +Pico, with three series resistors and a VGA connection, and DC power. +However, if you're not comfortable soldering then don't choose this as +your first project: I don't want you to zap your mouse + +Disclaimer: This is a hardware project with zero warranty. All due +care has been taken in design/docs, but if you choose to build it then +I disclaim any responsibility for your hardware or personal safety. + +With that out of the way... + +## Theory of operation + +Three 3.3V GPIO pins are driven by PIO to give VSYNC, HSYNC, and video +out signals. + +The syncs are in many similar projects driven directly from GPIO, but +here I suggest a 66Ω series resistor on each in order to keep the +voltages at the VGA end (presumably into 75Ω termination?) in the +correct range. + +For the video output, one GPIO drives R,G,B channels for mono/white +output. A 100Ω resistor gives roughly 0.7V (max intensity) into 3*75Ω +signals. + +That's it... power in, USB adapter. + + +## Pinout and circuit + +Parts needed: + + * Pico/RP2040 board + * USB OTG micro-B to A adapter + * USB keyboard, mouse (and hub, if not integrated) + * 5V DC supply (600mA+), and maybe a DC jack + * 100Ω resistor + * 2x 66Ω resistors + * VGA DB15 connector, or janky chopped VGA cable + +Pins are given for a RPi Pico board, but this will work on any RP2040 +board with 2MB+ flash as long as all required GPIOs are pinned out: + +| GPIO/pin | Pico pin | Usage | +| ------------ | ------------ | -------------- | +| GP0 | 1 | UART0 TX | +| GP1 | 2 | UART0 RX | +| GP18 | 24 | Video output | +| GP19 | 25 | VSYNC | +| GP21 | 27 | HSYNC | +| Gnd | 23, 28 | Video ground | +| VBUS (5V) | 40 | +5V supply | +| Gnd | 38 | Supply ground | + +Method: + + * Wire 5V supply to VBUS/Gnd + * Video output --> 100Ω --> VGA RGB (pins 1,2,3) all connected together + * HSYNC --> 66Ω --> VGA pin 13 + * VSYNC --> 66Ω --> VGA pin 14 + * Video ground --> VGA grounds (pins 5-8, 10) + +If you don't have exactly 100Ω, using slightly more is OK but display +will be dimmer. If you don't have 66Ω for the syncs, connecting them +directly is "probably OK", but YMMV. + +Test your connections: the key part is not getting over 0.7V into your +VGA connector's signals. + +Connect USB mouse, and keyboard if you like, and power up. + +# Software + +Both CPU cores are used, and are overclocked (blush) to 250MHz so that +Missile Command is enjoyable to play. + +The `umac` emulator and video output runs on core 1, and core 0 deals +with USB HID input. Video DMA is initialised pointing to the +framebuffer in the Mac's RAM. + +Other than that, it's just a main loop in `main.c` shuffling things +into `umac`. + +Quite a lot of optimisation has been done in `umac` and `Musashi` to +get performance up on Cortex-M0+ and the RP2040, like careful location +of certain routines in RAM, ensuring inlining/constants can be +foldeed, etc. It's 5x faster than it was at the beginning. + +The top-level project might be a useful framework for other emulators, +or other projects that need USB HID input and a framebuffer (e.g. a +VT220 emulator!). + +The USB HID code is largely stolen from the TinyUSB example, but shows +how in practice you might capture keypresses/deal with mouse events. + +## Video + +The video system is pretty good and IMHO worth stealing for other +projects: It uses one PIO state machine and 3 DMA channels to provide +a rock-solid bitmapped 1BPP 640x480 video output. The Mac 512x342 +framebuffer is centred inside this by using horizontal blanking +regions (programmed into the line scan-out) and vertical blanking +areas from a dummy "always black" mini-framebuffer. + +It supports (at build time) flexible resolutions/timings. The one +caveat (or advantage?) is that it uses an HSYNC IRQ routine to +recalculate the next DMA buffer pointer; doing this at scan-time costs +about 1% of the CPU time (on core 1). However, it could be used to +generate video on-the-fly from characters/tiles without a true +framebuffer. + +I'm considering improvements to the video system: + + * Supporting multiple BPP/colour output + * Implement the rest of `DE`/display valid strobe support, making + driving LCDs possible. + * Using a video DMA address list and another DMA channel to reduce + the IRQ frequency (CPU overhead) to per-frame, at the cost of a + couple of KB of RAM. + + +# Licence + +`hid.c` and `tusb_config.h` are based on code from the TinyUSB +project, which is Copyright (c) 2019, 2021 Ha Thach (tinyusb.org) and +released under the MIT licence. + +The remainder of the code is released under the MIT licence: + + Copyright (c) 2024 Matt Evans: + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + diff --git a/external/umac b/external/umac new file mode 160000 index 0000000..563626a --- /dev/null +++ b/external/umac @@ -0,0 +1 @@ +Subproject commit 563626a38273656ff08d13a4799fa5d1028b215d diff --git a/include/hw.h b/include/hw.h new file mode 100644 index 0000000..26f208c --- /dev/null +++ b/include/hw.h @@ -0,0 +1,37 @@ +/* + * pico-umac pin definitions + * + * Copyright 2024 Matt Evans + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef HW_H +#define HW_H + +#define GPIO_LED_PIN PICO_DEFAULT_LED_PIN + +#define GPIO_VID_DATA 18 +#define GPIO_VID_VS 19 +#define GPIO_VID_CLK 20 +#define GPIO_VID_HS 21 + +#endif diff --git a/include/kbd.h b/include/kbd.h new file mode 100644 index 0000000..fbc4243 --- /dev/null +++ b/include/kbd.h @@ -0,0 +1,40 @@ +/* + * pico-umac keyboard scancode mapping + * + * Copyright 2024 Matt Evans + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KBD_H +#define KBD_H + +#include +#include + +bool kbd_queue_empty(); +/* If empty, return 0, else return a mac keycode in [7:0] and [15] set if a press (else release) */ +uint16_t kbd_queue_pop(); + +/* FIXME: map modifiers */ +bool kbd_queue_push(uint8_t hid_keycode, bool pressed); + +#endif diff --git a/include/tusb_config.h b/include/tusb_config.h new file mode 100644 index 0000000..ba1b92f --- /dev/null +++ b/include/tusb_config.h @@ -0,0 +1,94 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus + extern "C" { +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU + #error CFG_TUSB_MCU must be defined +#endif + +#if CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX + #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_HOST | OPT_MODE_HIGH_SPEED) +#else + #define CFG_TUSB_RHPORT0_MODE OPT_MODE_HOST +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +// CFG_TUSB_DEBUG is defined by compiler in DEBUG build +// #define CFG_TUSB_DEBUG 0 + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// CONFIGURATION +//-------------------------------------------------------------------- + +// Size of buffer to hold descriptors and other data used for enumeration +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 4 // typical keyboard + mouse device can have 3-4 HID interfaces +#define CFG_TUH_MSC 0 +#define CFG_TUH_VENDOR 0 + +// max device support (excluding hub device) +#define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports + +//------------- HID -------------// +#define CFG_TUH_HID_EPIN_BUFSIZE 64 +#define CFG_TUH_HID_EPOUT_BUFSIZE 64 + +#ifdef __cplusplus + } +#endif + +#endif /* _TUSB_CONFIG_H_ */ diff --git a/include/video.h b/include/video.h new file mode 100644 index 0000000..9545679 --- /dev/null +++ b/include/video.h @@ -0,0 +1,33 @@ +/* PIO video output + * + * Copyright 2024 Matt Evans + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef VIDEO_H +#define VIDEO_H + +#include + +void video_init(uint32_t *framebuffer); + +#endif diff --git a/pico_sdk_import.cmake b/pico_sdk_import.cmake new file mode 100644 index 0000000..28efe9e --- /dev/null +++ b/pico_sdk_import.cmake @@ -0,0 +1,62 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +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") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/src/hid.c b/src/hid.c new file mode 100644 index 0000000..caae549 --- /dev/null +++ b/src/hid.c @@ -0,0 +1,290 @@ +/* + * Derived from pico-examples/usb/host/host_cdc_msc_hid/hid_app.c, which is + * Copyright (c) 2021, Ha Thach (tinyusb.org) + * Further changes are Copyright 2024 Matt Evans + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "bsp/rp2040/board.h" +#include "tusb.h" + +#include "kbd.h" + +//--------------------------------------------------------------------+ +// MACRO TYPEDEF CONSTANT ENUM DECLARATION +//--------------------------------------------------------------------+ + +// If your host terminal support ansi escape code such as TeraTerm +// it can be use to simulate mouse cursor movement within terminal +#define USE_ANSI_ESCAPE 0 + +#define MAX_REPORT 4 + +static uint8_t const keycode2ascii[128][2] = { HID_KEYCODE_TO_ASCII }; + +// Each HID instance can has multiple reports +static struct +{ + uint8_t report_count; + tuh_hid_report_info_t report_info[MAX_REPORT]; +} hid_info[CFG_TUH_HID]; + +static void process_kbd_report(hid_keyboard_report_t const *report); +static void process_mouse_report(hid_mouse_report_t const * report); +static void process_generic_report(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + +void hid_app_task(void) +{ + // nothing to do +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +// Invoked when device with hid interface is mounted +// Report descriptor is also available for use. tuh_hid_parse_report_descriptor() +// can be used to parse common/simple enough descriptor. +// Note: if report descriptor length > CFG_TUH_ENUMERATION_BUFSIZE, it will be skipped +// therefore report_desc = NULL, desc_len = 0 +void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) +{ + printf("HID device address = %d, instance = %d is mounted\r\n", dev_addr, instance); + + // Interface protocol (hid_interface_protocol_enum_t) + const char* protocol_str[] = { "None", "Keyboard", "Mouse" }; + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + + printf("HID Interface Protocol = %s\r\n", protocol_str[itf_protocol]); + + // By default host stack will use activate boot protocol on supported interface. + // Therefore for this simple example, we only need to parse generic report descriptor (with built-in parser) + if ( itf_protocol == HID_ITF_PROTOCOL_NONE ) + { + hid_info[instance].report_count = tuh_hid_parse_report_descriptor(hid_info[instance].report_info, MAX_REPORT, desc_report, desc_len); + printf("HID has %u reports \r\n", hid_info[instance].report_count); + } + + // request to receive report + // tuh_hid_report_received_cb() will be invoked when report is available + if ( !tuh_hid_receive_report(dev_addr, instance) ) + { + printf("Error: cannot request to receive report\r\n"); + } +} + +// Invoked when device with hid interface is un-mounted +void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) +{ + printf("HID device address = %d, instance = %d is unmounted\r\n", dev_addr, instance); +} + +// Invoked when received report from device via interrupt endpoint +void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) +{ + uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance); + + switch (itf_protocol) + { + case HID_ITF_PROTOCOL_KEYBOARD: + TU_LOG2("HID receive boot keyboard report\r\n"); + process_kbd_report( (hid_keyboard_report_t const*) report ); + break; + + case HID_ITF_PROTOCOL_MOUSE: + TU_LOG2("HID receive boot mouse report\r\n"); + process_mouse_report( (hid_mouse_report_t const*) report ); + break; + + default: + // Generic report requires matching ReportID and contents with previous parsed report info + process_generic_report(dev_addr, instance, report, len); + break; + } + + // continue to request to receive report + if ( !tuh_hid_receive_report(dev_addr, instance) ) + { + printf("Error: cannot request to receive report\r\n"); + } +} + +//--------------------------------------------------------------------+ +// Keyboard +//--------------------------------------------------------------------+ + +static inline bool find_key_in_report(hid_keyboard_report_t const *report, uint8_t keycode) +{ + for(uint8_t i=0; i<6; i++) + { + if (report->keycode[i] == keycode) return true; + } + + return false; +} + +static void process_kbd_report(hid_keyboard_report_t const *report) +{ + /* Previous report is stored to compare against for key release: */ + static hid_keyboard_report_t prev_report = { 0, 0, {0} }; + + for(uint8_t i=0; i<6; i++) { + if (report->keycode[i]) { + if (find_key_in_report(&prev_report, report->keycode[i])) { + /* Key held */ + } else { + /* printf("Key pressed: %02x\n", report->keycode[i]); */ + kbd_queue_push(report->keycode[i], true); + } + } + if (prev_report.keycode[i] && !find_key_in_report(report, prev_report.keycode[i])) { + /* printf("Key released: %02x\n", prev_report.keycode[i]); */ + kbd_queue_push(prev_report.keycode[i], false); + } + } + uint8_t mod_change = report->modifier ^ prev_report.modifier; + if (mod_change) { + uint8_t mp = mod_change & report->modifier; + uint8_t mr = mod_change & prev_report.modifier; + if (mp) { + /* printf("Modifiers pressed %02x\n", mp); */ + mp = (mp | (mp >> 4)) & 0xf; /* Don't care if left or right :P */ + if (mp & 1) + kbd_queue_push(HID_KEY_CONTROL_LEFT, true); + if (mp & 2) + kbd_queue_push(HID_KEY_SHIFT_LEFT, true); + if (mp & 4) + kbd_queue_push(HID_KEY_ALT_LEFT, true); + if (mp & 8) + kbd_queue_push(HID_KEY_GUI_LEFT, true); + } + if (mr) { + /* printf("Modifiers released %02x\n", mr); */ + mr = (mr | (mr >> 4)) & 0xf; + if (mr & 1) + kbd_queue_push(HID_KEY_CONTROL_LEFT, false); + if (mr & 2) + kbd_queue_push(HID_KEY_SHIFT_LEFT, false); + if (mr & 4) + kbd_queue_push(HID_KEY_ALT_LEFT, false); + if (mr & 8) + kbd_queue_push(HID_KEY_GUI_LEFT, false); + } + } + prev_report = *report; +} + +//--------------------------------------------------------------------+ +// Mouse +//--------------------------------------------------------------------+ + +/* Exported for use by other thread! */ +int cursor_x = 0; +int cursor_y = 0; +int cursor_button = 0; + +#define MAX_DELTA 8 + +static int clamp(int i) +{ + return (i >= 0) ? (i > MAX_DELTA ? MAX_DELTA : i) : + (i < -MAX_DELTA ? -MAX_DELTA : i); +} + +static void process_mouse_report(hid_mouse_report_t const * report) +{ + static hid_mouse_report_t prev_report = { 0 }; + uint8_t button_changed_mask = report->buttons ^ prev_report.buttons; + /* report->wheel can be used too... */ + + cursor_button = !!(report->buttons & MOUSE_BUTTON_LEFT); + cursor_x += clamp(report->x); + cursor_y += clamp(report->y); +} + +//--------------------------------------------------------------------+ +// Generic Report +//--------------------------------------------------------------------+ +static void process_generic_report(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) +{ + (void) dev_addr; + + uint8_t const rpt_count = hid_info[instance].report_count; + tuh_hid_report_info_t* rpt_info_arr = hid_info[instance].report_info; + tuh_hid_report_info_t* rpt_info = NULL; + + if ( rpt_count == 1 && rpt_info_arr[0].report_id == 0) + { + // Simple report without report ID as 1st byte + rpt_info = &rpt_info_arr[0]; + }else + { + // Composite report, 1st byte is report ID, data starts from 2nd byte + uint8_t const rpt_id = report[0]; + + // Find report id in the arrray + for(uint8_t i=0; iusage_page == HID_USAGE_PAGE_DESKTOP ) + { + switch (rpt_info->usage) + { + case HID_USAGE_DESKTOP_KEYBOARD: + TU_LOG1("HID receive keyboard report\r\n"); + // Assume keyboard follow boot report layout + process_kbd_report( (hid_keyboard_report_t const*) report ); + break; + + case HID_USAGE_DESKTOP_MOUSE: + TU_LOG1("HID receive mouse report\r\n"); + // Assume mouse follow boot report layout + process_mouse_report( (hid_mouse_report_t const*) report ); + break; + + default: break; + } + } +} diff --git a/src/kbd.c b/src/kbd.c new file mode 100644 index 0000000..b27947f --- /dev/null +++ b/src/kbd.c @@ -0,0 +1,199 @@ +/* HID to Mac keyboard scancode mapping + * + * FIXME: This doesn't do capslock (needs to track toggle), and arrow + * keys don't work. + * + * Copyright 2024 Matt Evans + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include "kbd.h" + +#include "class/hid/hid.h" +#include "keymap.h" + +#define KQ_SIZE 32 +#define KQ_MASK (KQ_SIZE-1) + +static uint16_t kbd_queue[KQ_SIZE]; +static unsigned int kbd_queue_prod = 0; +static unsigned int kbd_queue_cons = 0; + +static bool kbd_queue_full() +{ + return ((kbd_queue_prod + 1) & KQ_MASK) == kbd_queue_cons; +} + + +bool kbd_queue_empty() +{ + return kbd_queue_prod == kbd_queue_cons; +} + +/* If empty, return 0, else return a mac keycode in [7:0] and [15] set if a press (else release) */ +uint16_t kbd_queue_pop() +{ + if (kbd_queue_empty()) + return 0; + uint16_t v = kbd_queue[kbd_queue_cons]; + kbd_queue_cons = (kbd_queue_cons + 1) & KQ_MASK; + return v; +} + +static const uint8_t hid_to_mac[256] = { + [HID_KEY_NONE] = 0, + [HID_KEY_A] = 255, // Hack for MKC_A, + [HID_KEY_B] = MKC_B, + [HID_KEY_C] = MKC_C, + [HID_KEY_D] = MKC_D, + [HID_KEY_E] = MKC_E, + [HID_KEY_F] = MKC_F, + [HID_KEY_G] = MKC_G, + [HID_KEY_H] = MKC_H, + [HID_KEY_I] = MKC_I, + [HID_KEY_J] = MKC_J, + [HID_KEY_K] = MKC_K, + [HID_KEY_L] = MKC_L, + [HID_KEY_M] = MKC_M, + [HID_KEY_N] = MKC_N, + [HID_KEY_O] = MKC_O, + [HID_KEY_P] = MKC_P, + [HID_KEY_Q] = MKC_Q, + [HID_KEY_R] = MKC_R, + [HID_KEY_S] = MKC_S, + [HID_KEY_T] = MKC_T, + [HID_KEY_U] = MKC_U, + [HID_KEY_V] = MKC_V, + [HID_KEY_W] = MKC_W, + [HID_KEY_X] = MKC_X, + [HID_KEY_Y] = MKC_Y, + [HID_KEY_Z] = MKC_Z, + [HID_KEY_1] = MKC_1, + [HID_KEY_2] = MKC_2, + [HID_KEY_3] = MKC_3, + [HID_KEY_4] = MKC_4, + [HID_KEY_5] = MKC_5, + [HID_KEY_6] = MKC_6, + [HID_KEY_7] = MKC_7, + [HID_KEY_8] = MKC_8, + [HID_KEY_9] = MKC_9, + [HID_KEY_0] = MKC_0, + [HID_KEY_ENTER] = MKC_Return, + [HID_KEY_ESCAPE] = MKC_Escape, + [HID_KEY_BACKSPACE] = MKC_BackSpace, + [HID_KEY_TAB] = MKC_Tab, + [HID_KEY_SPACE] = MKC_Space, + [HID_KEY_MINUS] = MKC_Minus, + [HID_KEY_EQUAL] = MKC_Equal, + [HID_KEY_BRACKET_LEFT] = MKC_LeftBracket, + [HID_KEY_BRACKET_RIGHT] = MKC_RightBracket, + [HID_KEY_BACKSLASH] = MKC_BackSlash, + [HID_KEY_SEMICOLON] = MKC_SemiColon, + [HID_KEY_APOSTROPHE] = MKC_SingleQuote, + [HID_KEY_GRAVE] = MKC_Grave, + [HID_KEY_COMMA] = MKC_Comma, + [HID_KEY_PERIOD] = MKC_Period, + [HID_KEY_SLASH] = MKC_Slash, + [HID_KEY_CAPS_LOCK] = MKC_CapsLock, + [HID_KEY_F1] = MKC_F1, + [HID_KEY_F2] = MKC_F2, + [HID_KEY_F3] = MKC_F3, + [HID_KEY_F4] = MKC_F4, + [HID_KEY_F5] = MKC_F5, + [HID_KEY_F6] = MKC_F6, + [HID_KEY_F7] = MKC_F7, + [HID_KEY_F8] = MKC_F8, + [HID_KEY_F9] = MKC_F9, + [HID_KEY_F10] = MKC_F10, + [HID_KEY_F11] = MKC_F11, + [HID_KEY_F12] = MKC_F12, + [HID_KEY_PRINT_SCREEN] = MKC_Print, + [HID_KEY_SCROLL_LOCK] = MKC_ScrollLock, + [HID_KEY_PAUSE] = MKC_Pause, + [HID_KEY_INSERT] = MKC_Help, + [HID_KEY_HOME] = MKC_Home, + [HID_KEY_PAGE_UP] = MKC_PageUp, + [HID_KEY_DELETE] = MKC_BackSpace, + [HID_KEY_END] = MKC_End, + [HID_KEY_PAGE_DOWN] = MKC_PageDown, + [HID_KEY_ARROW_RIGHT] = MKC_Right, + [HID_KEY_ARROW_LEFT] = MKC_Left, + [HID_KEY_ARROW_DOWN] = MKC_Down, + [HID_KEY_ARROW_UP] = MKC_Up, + /* [HID_KEY_NUM_LOCK] = MKC_, */ + [HID_KEY_KEYPAD_DIVIDE] = MKC_KPDevide, + [HID_KEY_KEYPAD_MULTIPLY] = MKC_KPMultiply, + [HID_KEY_KEYPAD_SUBTRACT] = MKC_KPSubtract, + [HID_KEY_KEYPAD_ADD] = MKC_KPAdd, + [HID_KEY_KEYPAD_ENTER] = MKC_Enter, + [HID_KEY_KEYPAD_1] = MKC_KP1, + [HID_KEY_KEYPAD_2] = MKC_KP2, + [HID_KEY_KEYPAD_3] = MKC_KP3, + [HID_KEY_KEYPAD_4] = MKC_KP4, + [HID_KEY_KEYPAD_5] = MKC_KP5, + [HID_KEY_KEYPAD_6] = MKC_KP6, + [HID_KEY_KEYPAD_7] = MKC_KP7, + [HID_KEY_KEYPAD_8] = MKC_KP8, + [HID_KEY_KEYPAD_9] = MKC_KP9, + [HID_KEY_KEYPAD_0] = MKC_KP0, + [HID_KEY_KEYPAD_DECIMAL] = MKC_Decimal, + [HID_KEY_KEYPAD_EQUAL] = MKC_Equal, + [HID_KEY_RETURN] = MKC_Return, + /* [HID_KEY_POWER] = MKC_, */ + /* [HID_KEY_KEYPAD_COMMA] = MKC_, */ + /* [HID_KEY_KEYPAD_EQUAL_SIGN] = MKC_, */ + [HID_KEY_CONTROL_LEFT] = MKC_Control, + [HID_KEY_SHIFT_LEFT] = MKC_Shift, + [HID_KEY_ALT_LEFT] = MKC_Option, + [HID_KEY_GUI_LEFT] = MKC_Command, + [HID_KEY_CONTROL_RIGHT] = MKC_Control, + [HID_KEY_SHIFT_RIGHT] = MKC_Shift, + [HID_KEY_ALT_RIGHT] = MKC_Option, + [HID_KEY_GUI_RIGHT] = MKC_Command, +}; + +static bool kbd_map(uint8_t hid_keycode, bool pressed, uint16_t *key_out) +{ + uint8_t k = hid_to_mac[hid_keycode]; + if (!k) + return false; + if (k == 255) + k = MKC_A; // Hack, this is zero + k = (k << 1) | 1; // FIXME just do this in the #defines + *key_out = k | (pressed ? 0x8000 : 0); /* Convention w.r.t. main */ + return true; +} + +bool kbd_queue_push(uint8_t hid_keycode, bool pressed) +{ + if (kbd_queue_full()) + return false; + + uint16_t v; + if (!kbd_map(hid_keycode, pressed, &v)) + return false; + + kbd_queue[kbd_queue_prod] = v; + kbd_queue_prod = (kbd_queue_prod + 1) & KQ_MASK; + return true; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..684c23d --- /dev/null +++ b/src/main.c @@ -0,0 +1,181 @@ +/* pico-umac + * + * Main loop to initialise umac, and run main event loop (piping + * keyboard/mouse events in). + * + * Copyright 2024 Matt Evans + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include "hardware/gpio.h" +#include "hardware/pio.h" +#include "hardware/sync.h" +#include "pico/multicore.h" +#include "pico/stdlib.h" +#include "pico/time.h" +#include "hw.h" +#include "video.h" +#include "kbd.h" + +#include "bsp/rp2040/board.h" +#include "tusb.h" + +#include "umac.h" + +//////////////////////////////////////////////////////////////////////////////// +// Imports and data + +extern void hid_app_task(void); +extern int cursor_x; +extern int cursor_y; +extern int cursor_button; + +// Mac binary data: disc and ROM images +static const uint8_t umac_disc[] = { +#include "umac-disc.h" +}; +static const uint8_t umac_rom[] = { +#include "umac-rom.h" +}; + +static uint8_t umac_ram[RAM_SIZE]; + +//////////////////////////////////////////////////////////////////////////////// + +static void io_init() +{ + gpio_init(GPIO_LED_PIN); + gpio_set_dir(GPIO_LED_PIN, GPIO_OUT); +} + +static void poll_led_etc() +{ + static int led_on = 0; + static absolute_time_t last = 0; + absolute_time_t now = get_absolute_time(); + + if (absolute_time_diff_us(last, now) > 500*1000) { + last = now; + + led_on ^= 1; + gpio_put(GPIO_LED_PIN, led_on); + } +} + +static int umac_cursor_x = 0; +static int umac_cursor_y = 0; +static int umac_cursor_button = 0; + +static void poll_umac() +{ + static absolute_time_t last_1hz = 0; + static absolute_time_t last_vsync = 0; + absolute_time_t now = get_absolute_time(); + + umac_loop(); + + int64_t p_1hz = absolute_time_diff_us(last_1hz, now); + int64_t p_vsync = absolute_time_diff_us(last_vsync, now); + if (p_vsync >= 16667) { + /* FIXME: Trigger this off actual vsync */ + umac_vsync_event(); + last_vsync = now; + } + if (p_1hz >= 1000000) { + umac_1hz_event(); + last_1hz = now; + } + + int update = 0; + int dx = 0; + int dy = 0; + int b = umac_cursor_button; + if (cursor_x != umac_cursor_x) { + dx = cursor_x - umac_cursor_x; + umac_cursor_x = cursor_x; + update = 1; + } + if (cursor_y != umac_cursor_y) { + dy = cursor_y - umac_cursor_y; + umac_cursor_y = cursor_y; + update = 1; + } + if (cursor_button != umac_cursor_button) { + b = cursor_button; + umac_cursor_button = cursor_button; + update = 1; + } + if (update) { + umac_mouse(dx, -dy, b); + } + + if (!kbd_queue_empty()) { + uint16_t k = kbd_queue_pop(); + umac_kbd_event(k & 0xff, !!(k & 0x8000)); + } +} + +static void core1_main() +{ + printf("Core 1 started\n"); + + disc_descr_t discs[DISC_NUM_DRIVES] = {0}; + discs[0].base = (void *)umac_disc; + discs[0].read_only = 1; + discs[0].size = sizeof(umac_disc); + + umac_init(umac_ram, (void *)umac_rom, discs); + /* Video runs on core 1, i.e. IRQs/DMA are unaffected by + * core 0's USB activity. + */ + video_init((uint32_t *)(umac_ram + umac_get_fb_offset())); + + while (true) { + poll_umac(); + } +} + +int main() +{ + set_sys_clock_khz(250*1000, true); + + stdio_init_all(); + io_init(); + + multicore_launch_core1(core1_main); + + printf("Starting, init usb\n"); + tusb_init(); + + /* This happens on core 0: */ + while (true) { + tuh_task(); + hid_app_task(); + poll_led_etc(); + } + + return 0; +} + diff --git a/src/pio_video.pio b/src/pio_video.pio new file mode 100644 index 0000000..1750b7c --- /dev/null +++ b/src/pio_video.pio @@ -0,0 +1,194 @@ +; PIO video output: +; This scans out video lines, characteristically some number of bits per pixel, +; a pixel clock, and timing signals HSync, VSync (in future, DE too). +; +; Copyright 2024 Matt Evans +; +; Permission is hereby granted, free of charge, to any person +; obtaining a copy of this software and associated documentation files +; (the "Software"), to deal in the Software without restriction, +; including without limitation the rights to use, copy, modify, merge, +; publish, distribute, sublicense, and/or sell copies of the Software, +; and to permit persons to whom the Software is furnished to do so, +; subject to the following conditions: +; +; The above copyright notice and this permission notice shall be +; included in all copies or substantial portions of the Software. +; +; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +; BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +; ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +; SOFTWARE. +; +; +; The source of image data is the OUT FIFO, and a corresponding C routine +; needs to fill this with image data. That data might be generated on +; the fly, or constructed by setting DMA pointers to a framebuffer. +; +; A typical usage would be the C routine preparing one scanline of data +; and setting off DMA. That's a good balance between number of interrupts +; and amount of buffering/RAM required (a framebuffer is generally pretty +; large...) +; +; Supports a max of 15bpp. That's tons given the number of IO... expecting +; to use this with 8, 3, etc. +; +; The output pins are required to be, in this order, +; 0: Video data +; 0+BPP: Vsync +; 1+BPP: PClk +; 2+BPP: Hsync +; +; FIXME: 1BPP for now +; +; The horizontal timing information is embedded in the data read via +; the FIFO, as follows, shown from the very start of a frame. The vertical +; timing info is generated entirely from the C side by passing a VSync +; value in the data stream. The data stream for each line is: +; +; ---------- Config information: (offset 0 on each line) ---------- +; 32b: Timing/sync info: +; [31] Vsync value for this line +; [30:23] Hsync width (HSW) +; [22:15] HBP width minus 3 (FIXME: check) +; [14:7] HFP width minus 3 +; 32b: Number of visible pixels per line +; ---------- Pixel data: (offset 8 on each line) ------------------- +; : video data (padded with zeros for HBP/HFP pixels) +; ------------------------------------------------------------------- +; +; + +-------------------------------------------------- +; | |HBP- -HFP +; +-+ +; +--+ ***************************************************** +; | ***************************************************** +; +--+ ***************************************************** +; |VBP ***************************************************** +; | &&&&&&&+----------------------------------------+%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| Active area |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&| |%%%% +; | &&&&&&&+----------------------------------------+%%%% +; |VFP @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +; |: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +; |: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +; +; The HFP/HBP pixels should be written zero. Clever DMA programming can +; provide these from a separate location to the video data. +; +; FIXME: Add DE (and therefore full HBP/HFP counters in timing word) +; +; There are a couple of pin-mapping tricks going on. We need to be +; able to change the video without messing wtih VS, and we want to +; assert HS/VS at the same instant. That means the syncs aren't part +; of the OUT mapping -- this is only the pixel data. The SET mapping +; controls VS, and SIDESET controls HS/clk. +; +; The advantage of OUT being solely for data is then being able to easily +; extend to multiple BPP. Note the HS/VS are active-high in terms of +; programming, but the output signal can be flipped at GPIO using the +; inversion feature. + +; .define BPP 123 etc. + +.program pio_video +.side_set 2 ; SS[0] is clk, SS[1] is HS + +frame_start: +; The first word gives VS/HSW: [31]=vsync; [30:23]=HSW +; [22:15]=HBP, [14:7]=HFP. (shifted left!) +; +; Set VS on the same cycle as asserting HS. +; Note: these cycles are part of HFP. + out X, 1 side 0 + jmp !X, vs_inactive side 1 +vs_active: + set pins, 1 side 2 + jmp now_read_HSW side 3 +vs_inactive: + set pins, 0 side 2 + nop side 3 + +now_read_HSW: ; X=hsync width: + out X, 8 side 2 +hsw_loop: nop side 3 + jmp X-- hsw_loop side 2 + +; De-assert hsync (leave Vsync as-is) and shift out HBP: +now_read_HBP: ; X=HBP width: + out X, 8 side 1 +hbp_loop: nop side 0 + jmp X-- hbp_loop side 1 + +now_read_HFP: ; Y=HFP width: + out Y, 8 side 0 + +; Pull, discarding the remainder of OSR. This prepares X pixel count. +; Note: these cycles (and HFP read) are part of HBP. + pull block side 1 + out X, 32 side 0 + nop side 1 +pixels_loop: ; OSR primed/autopulled + ; FIXME: side-set DE=1 + out pins, 1 side 0 ; BPP + jmp X-- pixels_loop side 1 + ; FIXME: side-set DE=0 + ; Set video BLACK (1) + mov pins, !NULL side 0 + nop side 1 +; Now perform HFP delay +hfp_loop: nop side 0 + jmp Y-- hfp_loop side 1 + +; A free HFP pixel, to prime for next line: + // Auto-pull gets next line (always a multiple of 32b) + nop side 0 + jmp frame_start side 1 + +; HFP 2 min +; HBP 2 min + + +% c-sdk { +static inline void pio_video_program_init(PIO pio, uint sm, uint offset, + uint video_pin /* then VS, CLK, HS */, + float clk_div) { + /* Outputs are consecutive up from Video data */ + uint vsync_pin = video_pin+1; + uint clk_pin = video_pin+2; + uint hsync_pin = video_pin+3; + /* Init GPIO & directions */ + pio_gpio_init(pio, video_pin); + pio_gpio_init(pio, hsync_pin); + pio_gpio_init(pio, vsync_pin); + pio_gpio_init(pio, clk_pin); + // FIXME: BPP define + pio_sm_set_consecutive_pindirs(pio, sm, video_pin, 4, true /* out */); + + pio_sm_config c = pio_video_program_get_default_config(offset); + sm_config_set_out_pins(&c, video_pin, 1); + sm_config_set_set_pins(&c, vsync_pin, 1); + sm_config_set_sideset_pins(&c, clk_pin); /* CLK + HS */ + /* Sideset bits are configured via .side_set directive above */ + sm_config_set_out_shift(&c, false /* OUT MSBs first */, true /* Autopull */, 32 /* bits */); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + sm_config_set_clkdiv(&c, clk_div); + + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); +} +%} diff --git a/src/video.c b/src/video.c new file mode 100644 index 0000000..1e98e78 --- /dev/null +++ b/src/video.c @@ -0,0 +1,344 @@ +/* Video output: + * + * Using PIO[1], output the Mac 512x342 1BPP framebuffer to VGA/pins. This is done + * directly from the Mac framebuffer (without having to reformat in an intermediate + * buffer). The video output is 640x480, with the visible pixel data centred with + * borders: for analog VGA this is easy, as it just means increasing the horizontal + * back porch/front porch (time between syncs and active video) and reducing the + * display portion of a line. + * + * [1]: see pio_video.pio + * + * Copyright 2024 Matt Evans + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include "hardware/clocks.h" +#include "hardware/dma.h" +#include "hardware/gpio.h" +#include "pio_video.pio.h" + +#include "hw.h" + +//////////////////////////////////////////////////////////////////////////////// +/* VESA VGA mode 640x480@60 */ + +/* The pixel clock _should_ be (125/2/25.175) (about 2.483) but that seems to + * make my VGA-HDMI adapter sample weird, and pixels crawl. Fudge a little, + * looks better: + */ +#define VIDEO_PCLK_MULT (2.5*2) +#define VIDEO_HSW 96 +#define VIDEO_HBP 48 +#define VIDEO_HRES 640 +#define VIDEO_HFP 16 +#define VIDEO_H_TOTAL_NOSYNC (VIDEO_HBP + VIDEO_HRES + VIDEO_HFP) +#define VIDEO_VSW 2 +#define VIDEO_VBP 33 +#define VIDEO_VRES 480 +#define VIDEO_VFP 10 +#define VIDEO_V_TOTAL (VIDEO_VSW + VIDEO_VBP + VIDEO_VRES + VIDEO_VFP) +/* The visible vertical span in the VGA output, [start, end) lines: */ +#define VIDEO_V_VIS_START (VIDEO_VSW + VIDEO_VBP) +#define VIDEO_V_VIS_END (VIDEO_V_VIS_START + VIDEO_VRES) + +#define VIDEO_FB_HRES 512 +#define VIDEO_FB_VRES 342 + +/* The lines at which the FB data is actively output: */ +#define VIDEO_FB_V_VIS_START (VIDEO_V_VIS_START + ((VIDEO_VRES - VIDEO_FB_VRES)/2)) +#define VIDEO_FB_V_VIS_END (VIDEO_FB_V_VIS_START + VIDEO_FB_VRES) + +/* Words of 1BPP pixel data per line; this dictates the length of the + * video data DMA transfer: + */ +#define VIDEO_VISIBLE_WPL (VIDEO_FB_HRES / 32) + +#if (VIDEO_HRES & 31) +#error "VIDEO_HRES: must be a multiple of 32b!" +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Video DMA, framebuffer pointers + +static uint32_t video_null[VIDEO_VISIBLE_WPL]; +static uint32_t *video_framebuffer; + +/* DMA buffer containing 2 pairs of per-line config words, for VS and not-VS: */ +static uint32_t video_dma_cfg[4]; + +/* 3 DMA channels are used. The first to transfer data to PIO, and + * the other two to transfer descriptors to the first channel. + */ +static uint8_t video_dmach_tx; +static uint8_t video_dmach_descr_cfg; +static uint8_t video_dmach_descr_data; + +typedef struct { + const void *raddr; + void *waddr; + uint32_t count; + uint32_t ctrl; +} dma_descr_t; + +static dma_descr_t video_dmadescr_cfg; +static dma_descr_t video_dmadescr_data; + +static volatile unsigned int video_current_y = 0; + +static int __not_in_flash_func(video_get_visible_y)(unsigned int y) { + if ((y >= VIDEO_FB_V_VIS_START) && (y < VIDEO_FB_V_VIS_END)) { + return y - VIDEO_FB_V_VIS_START; + } else { + return -1; + } +} + +static const uint32_t *__not_in_flash_func(video_line_addr)(unsigned int y) +{ + int vy = video_get_visible_y(y); + if (vy >= 0) + return (const uint32_t *)&video_framebuffer[vy * VIDEO_VISIBLE_WPL]; + else + return (const uint32_t *)video_null; +} + +static const uint32_t *__not_in_flash_func(video_cfg_addr)(unsigned int y) +{ + return &video_dma_cfg[(y < VIDEO_VSW) ? 0 : 2]; +} + +static void __not_in_flash_func(video_dma_prep_new)() +{ + /* The descriptor DMA read pointers have moved on; reset them. + * The write pointers wrap so should be pointing to the + * correct DMA regs. + */ + dma_hw->ch[video_dmach_descr_cfg].read_addr = (uintptr_t)&video_dmadescr_cfg; + dma_hw->ch[video_dmach_descr_cfg].transfer_count = 4; + dma_hw->ch[video_dmach_descr_data].read_addr = (uintptr_t)&video_dmadescr_data; + dma_hw->ch[video_dmach_descr_data].transfer_count = 4; + + /* Configure the two DMA descriptors, video_dmadescr_cfg and + * video_dmadescr_data, to transfer from video config/data corresponding + * to the current line. + * + * These descriptors will be used to program the video_dmach_tx channel, + * pushing the buffer to PIO. + * + * This can be relatively relaxed, as it's triggered as line data + * starts; we have until the end of the video line (when the descriptors + * are retriggered) to program them. + * + * FIXME: this time could be used for something clever like split-screen + * (e.g. info/text lines) constructed on-the-fly. + */ + video_dmadescr_cfg.raddr = video_cfg_addr(video_current_y); + video_dmadescr_data.raddr = video_line_addr(video_current_y); + + /* Frame done */ + if (++video_current_y >= VIDEO_V_TOTAL) + video_current_y = 0; +} + +static void __not_in_flash_func(video_dma_irq)() +{ + /* The DMA IRQ occurs once the video portion of the line has been + * triggered (not when the video transfer completes, but when the + * descriptor transfer (that leads to the video transfer!) completes. + * All we need to do is reconfigure the descriptors; the video DMA will + * re-trigger the descriptors later. + */ + if (dma_channel_get_irq0_status(video_dmach_descr_data)) { + dma_channel_acknowledge_irq0(video_dmach_descr_data); + video_dma_prep_new(); + } +} + +static void video_prep_buffer() +{ + memset(video_null, 0xff, VIDEO_VISIBLE_WPL * 4); + + unsigned int porch_padding = (VIDEO_HRES - VIDEO_FB_HRES)/2; + // FIXME: HBP/HFP are prob off by one or so, check + uint32_t timing = ((VIDEO_HSW - 1) << 23) | + ((VIDEO_HBP + porch_padding - 3) << 15) | + ((VIDEO_HFP + porch_padding - 4) << 7); + video_dma_cfg[0] = timing | 0x80000000; + video_dma_cfg[1] = VIDEO_FB_HRES - 1; + video_dma_cfg[2] = timing; + video_dma_cfg[3] = VIDEO_FB_HRES - 1; +} + +static void video_init_dma() +{ + /* pio_video expects each display line to be composed of two words of config + * describing the line geometry and whether VS is asserted, followed by + * visible data. + * + * To avoid having to embed config metadata in the display framebuffer, + * we use two DMA transfers to PIO for each line. The first transfers + * the config from a config buffer, and then triggers the second to + * transfer the video data from the framebuffer. (This lets us use a + * flat, regular FB.) + * + * The PIO side emits 1BPP MSB-first. The other advantage of + * using a second DMA transfer is then we can also can + * byteswap the DMA of the video portion to match the Mac + * framebuffer layout. + * + * "Another caveat is that multiple channels should not be connected + * to the same DREQ.": + * The final complexity is that only one DMA channel can do the + * transfers to PIO, because of how the credit-based flow control works. + * So, _only_ channel 0 transfers from $SOME_BUFFER into the PIO FIFO, + * and channel 1+2 are used to reprogram/trigger channel 0 from a DMA + * descriptor list. + * + * Two extra channels are used to manage interrupts; ch1 programs ch0, + * completes, and does nothing. (It programs a descriptor that causes + * ch0 to transfer config, then trigger ch2 when complete.) ch2 then + * programs ch0 with a descriptor to transfer data, then trigger ch1 + * when ch0 completes; when ch2 finishes doing that, it produces an IRQ. + * Got that? + * + * The IRQ handler sets up ch1 and ch2 to point to 2 fresh cfg+data + * descriptors; the deadline is by the end of ch0's data transfer + * (i.e. a whole line). When ch0 finishes the data transfer it again + * triggers ch1, and the new config entry is programmed. + */ + video_dmach_tx = dma_claim_unused_channel(true); + video_dmach_descr_cfg = dma_claim_unused_channel(true); + video_dmach_descr_data = dma_claim_unused_channel(true); + + /* Transmit DMA: config+video data */ + /* First, make dmacfg for data to transfer from config buffers + data buffers: */ + dma_channel_config dc_tx_c = dma_channel_get_default_config(video_dmach_tx); + channel_config_set_dreq(&dc_tx_c, DREQ_PIO0_TX0); + channel_config_set_transfer_data_size(&dc_tx_c, DMA_SIZE_32); + channel_config_set_read_increment(&dc_tx_c, true); + channel_config_set_write_increment(&dc_tx_c, false); + channel_config_set_bswap(&dc_tx_c, false); + /* Completion of the config TX triggers the video_dmach_descr_data channel */ + channel_config_set_chain_to(&dc_tx_c, video_dmach_descr_data); + video_dmadescr_cfg.raddr = NULL; /* Reprogrammed each line */ + video_dmadescr_cfg.waddr = (void *)&pio0_hw->txf[0]; + video_dmadescr_cfg.count = 2; /* 2 words of video config */ + video_dmadescr_cfg.ctrl = dc_tx_c.ctrl; + + dma_channel_config dc_tx_d = dma_channel_get_default_config(video_dmach_tx); + channel_config_set_dreq(&dc_tx_d, DREQ_PIO0_TX0); + channel_config_set_transfer_data_size(&dc_tx_d, DMA_SIZE_32); + channel_config_set_read_increment(&dc_tx_d, true); + channel_config_set_write_increment(&dc_tx_d, false); + channel_config_set_bswap(&dc_tx_d, true); /* This channel bswaps */ + /* Completion of the data TX triggers the video_dmach_descr_cfg channel */ + channel_config_set_chain_to(&dc_tx_d, video_dmach_descr_cfg); + video_dmadescr_data.raddr = NULL; /* Reprogrammed each line */ + video_dmadescr_data.waddr = (void *)&pio0_hw->txf[0]; + video_dmadescr_data.count = VIDEO_VISIBLE_WPL; + video_dmadescr_data.ctrl = dc_tx_d.ctrl; + + /* Now, the descr_cfg and descr_data channels transfer _those_ + * descriptors to program the video_dmach_tx channel: + */ + dma_channel_config dcfg = dma_channel_get_default_config(video_dmach_descr_cfg); + channel_config_set_transfer_data_size(&dcfg, DMA_SIZE_32); + channel_config_set_read_increment(&dcfg, true); + channel_config_set_write_increment(&dcfg, true); + /* This channel loops on 16-byte/4-wprd boundary (i.e. writes all config): */ + channel_config_set_ring(&dcfg, true, 4); + /* No completion IRQ or chain: the video_dmach_tx DMA completes and triggers + * the next 'data' descriptor transfer. + */ + dma_channel_configure(video_dmach_descr_cfg, &dcfg, + &dma_hw->ch[video_dmach_tx].read_addr, + &video_dmadescr_cfg, + 4 /* 4 words of config */, + false /* Not yet */); + + dma_channel_config ddata = dma_channel_get_default_config(video_dmach_descr_data); + channel_config_set_transfer_data_size(&ddata, DMA_SIZE_32); + channel_config_set_read_increment(&ddata, true); + channel_config_set_write_increment(&ddata, true); + channel_config_set_ring(&ddata, true, 4); + /* This transfer has a completion IRQ. Receipt of that means that both + * config and data descriptors have been transferred, and should be + * reprogrammed for the next line. + */ + dma_channel_set_irq0_enabled(video_dmach_descr_data, true); + dma_channel_configure(video_dmach_descr_data, &ddata, + &dma_hw->ch[video_dmach_tx].read_addr, + &video_dmadescr_data, + 4 /* 4 words of config */, + false /* Not yet */); + + /* Finally, set up video_dmadescr_cfg.raddr and video_dmadescr_data.raddr to point + * to next line's video cfg/data buffers. Then, video_dmach_descr_cfg can be triggered + * to start video. + */ +} + +//////////////////////////////////////////////////////////////////////////////// + +/* Initialise PIO, DMA, start sending pixels. Passed a pointer to a 512x342x1 + * Mac-order framebuffer. + * + * FIXME: Add an API to change the FB base after init live, e.g. for bank + * switching. + */ +void video_init(uint32_t *framebuffer) +{ + printf("Video init\n"); + + pio_video_program_init(pio0, 0, + pio_add_program(pio0, &pio_video_program), + GPIO_VID_DATA, /* Followed by HS, VS, CLK */ + VIDEO_PCLK_MULT); + + /* Invert output pins: HS/VS are active-low, also invert video! */ + gpio_set_outover(GPIO_VID_HS, GPIO_OVERRIDE_INVERT); + gpio_set_outover(GPIO_VID_VS, GPIO_OVERRIDE_INVERT); + gpio_set_outover(GPIO_VID_DATA, GPIO_OVERRIDE_INVERT); + /* Highest drive strength (VGA is current-based, innit) */ + hw_write_masked(&padsbank0_hw->io[GPIO_VID_DATA], + PADS_BANK0_GPIO0_DRIVE_VALUE_12MA << PADS_BANK0_GPIO0_DRIVE_LSB, + PADS_BANK0_GPIO0_DRIVE_BITS); + + /* IRQ handlers for DMA_IRQ_0: */ + irq_set_exclusive_handler(DMA_IRQ_0, video_dma_irq); + irq_set_enabled(DMA_IRQ_0, true); + + video_init_dma(); + + /* Init config word buffers */ + video_current_y = 0; + video_framebuffer = framebuffer; + video_prep_buffer(); + + /* Set up pointers to first line, and start DMA */ + video_dma_prep_new(); + dma_channel_start(video_dmach_descr_cfg); +}