Initial release

Squashed, from v0.1
This commit is contained in:
Matt Evans 2024-05-07 23:41:25 +01:00
commit bd30ffe141
14 changed files with 1822 additions and 0 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "external/umac"]
url = https://github.com/evansm7/umac
path = external/umac

110
CMakeLists.txt Normal file
View file

@ -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()

234
README.md Normal file
View file

@ -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
<https://winworldpc.com/product/mac-os-0-6/system-3x> 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.

1
external/umac vendored Submodule

@ -0,0 +1 @@
Subproject commit 563626a38273656ff08d13a4799fa5d1028b215d

37
include/hw.h Normal file
View file

@ -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

40
include/kbd.h Normal file
View file

@ -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 <inttypes.h>
#include <stdbool.h>
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

94
include/tusb_config.h Normal file
View file

@ -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_ */

33
include/video.h Normal file
View file

@ -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 <inttypes.h>
void video_init(uint32_t *framebuffer);
#endif

62
pico_sdk_import.cmake Normal file
View file

@ -0,0 +1,62 @@
# This is a copy of <PICO_SDK_PATH>/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})

290
src/hid.c Normal file
View file

@ -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; i<rpt_count; i++)
{
if (rpt_id == rpt_info_arr[i].report_id )
{
rpt_info = &rpt_info_arr[i];
break;
}
}
report++;
len--;
}
if (!rpt_info)
{
printf("Couldn't find the report info for this report !\r\n");
return;
}
// For complete list of Usage Page & Usage checkout src/class/hid/hid.h. For examples:
// - Keyboard : Desktop, Keyboard
// - Mouse : Desktop, Mouse
// - Gamepad : Desktop, Gamepad
// - Consumer Control (Media Key) : Consumer, Consumer Control
// - System Control (Power key) : Desktop, System Control
// - Generic (vendor) : 0xFFxx, xx
if ( rpt_info->usage_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;
}
}
}

199
src/kbd.c Normal file
View file

@ -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 <stdio.h>
#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;
}

181
src/main.c Normal file
View file

@ -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 <stdio.h>
#include <unistd.h>
#include <string.h>
#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;
}

194
src/pio_video.pio Normal file
View file

@ -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) -------------------
; <X * bytes_per_pixel>: 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);
}
%}

344
src/video.c Normal file
View file

@ -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 <stdio.h>
#include <inttypes.h>
#include <string.h>
#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);
}