Initial release
Squashed, from v0.1
This commit is contained in:
commit
bd30ffe141
14 changed files with 1822 additions and 0 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "external/umac"]
|
||||
url = https://github.com/evansm7/umac
|
||||
path = external/umac
|
||||
110
CMakeLists.txt
Normal file
110
CMakeLists.txt
Normal 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
234
README.md
Normal 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
1
external/umac
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 563626a38273656ff08d13a4799fa5d1028b215d
|
||||
37
include/hw.h
Normal file
37
include/hw.h
Normal 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
40
include/kbd.h
Normal 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
94
include/tusb_config.h
Normal 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
33
include/video.h
Normal 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
62
pico_sdk_import.cmake
Normal 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
290
src/hid.c
Normal 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
199
src/kbd.c
Normal 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
181
src/main.c
Normal 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
194
src/pio_video.pio
Normal 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
344
src/video.c
Normal 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);
|
||||
}
|
||||
Loading…
Reference in a new issue