1713 lines
53 KiB
C++
1713 lines
53 KiB
C++
// Interfacing with the Gigatron TTL microcomputer using a microcontroller
|
|
//
|
|
// 4. Game Controller
|
|
// (optional)
|
|
// |
|
|
// v
|
|
// +-----------+
|
|
// Gigatron | | PC/laptop over USB (optional)
|
|
// controller <---| BabelFish |<--- 1. Send GT1 files
|
|
// port J4 | | 2. Serial console
|
|
// +-----------+
|
|
// ^
|
|
// |
|
|
// 3. PS/2 keyboard
|
|
// (optional)
|
|
//
|
|
// This sketch serves several purpuses:
|
|
// 1. Transfer a GT1 file into the Gigatron for execution.
|
|
// This can be done with two methods: over USB or from PROGMEM
|
|
// Each has their advantages and disadvantages:
|
|
// a. USB can accept a regular GT1 file but needs a (Python)
|
|
// program on the PC/laptop as sender (sendFile.py).
|
|
// b. PROGMEM only requires the Arduino IDE on the PC/laptop,
|
|
// but needs the GT1 file as a hexdump (C notation).
|
|
// 2. Hookup a PS/2 keyboard for typing on the Gigatron
|
|
// 3. Forward text from USB to Gigatron as keystrokes
|
|
// For example to get a long BASIC program loaded into BASIC
|
|
// 4. Controlling the Gigatron over USB from a PC/laptop
|
|
// 5. Passing through of game controller signals
|
|
// 6. Receive data from Gigatron and store it in the EEPROM area.
|
|
// If USB is connected, it also gets forwarded to the PC/laptop.
|
|
// 7. Sending the EEPROM data back into the Gigatron as a series
|
|
// of keystrokes.
|
|
//
|
|
// Select one of the supported platforms in the Tools->Board menu.
|
|
//
|
|
// Supported:
|
|
// - Arduino/Genuino Uno
|
|
// - Arduino Nano
|
|
// - Arduino/Genuino Micro
|
|
// - ATtiny85 (8 MHz)
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
| Preset configuarations |
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Arduino Uno config |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// Arduino AVR Gigatron Schematic Controller PCB Gigatron
|
|
// Uno Name OUT bit CD4021 74HC595 (U39) DB9 (J4)
|
|
// ------- ------ -------- --------- ---------- ---------------- --------
|
|
// Pin 13 PORTB5 None SER_DATA 11 SER INP 14 SER 2
|
|
// Pin 12 PORTB4 7 vSync SER_LATCH 0 PAR/SER None 3
|
|
// Pin 11 PORTB3 6 hSync SER_PULSE 10 CLOCK 11 SRCLK 12 RCLK 4
|
|
|
|
#if defined(ARDUINO_AVR_UNO)
|
|
#define platform "ArduinoUno"
|
|
#define maxStorage 32256
|
|
|
|
// Pinout reference:
|
|
// https://i2.wp.com/marcusjenkins.com/wp-content/uploads/2014/06/ARDUINO_V2.png
|
|
|
|
// Pins for Gigatron (must be on PORTB)
|
|
#define gigatronDataPin 13
|
|
#define gigatronLatchPin 12
|
|
#define gigatronPulsePin 11
|
|
#define gigatronPinToBitMask digitalPinToBitMask
|
|
|
|
// Pins for Controller
|
|
#define gameControllerDataPin -1
|
|
|
|
// Pins for PS/2 keyboard (Arduino Uno)
|
|
#define keyboardClockPin 3 // Pin 2 or 3 for IRQ
|
|
#define keyboardDataPin 4 // Any available free pin
|
|
|
|
// Link to PC/laptop
|
|
#define hasSerial 1
|
|
|
|
// SD Card
|
|
#define sdChipSelectPin -1
|
|
#endif
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Arduino Nano config |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// Arduino AVR Gigatron Schematic Controller PCB Gigatron Controller
|
|
// Nano Name OUT bit CD4021 74HC595 (U39) DB9 (J4) DB9
|
|
// --------- ------ -------- --------- ---------- ---------------- -------- -------
|
|
// Pin J2-15 PORTB5 None SER_DATA 11 SER INP 14 SER 2 None
|
|
// Pin J1-15 PORTB4 7 vSync SER_LATCH 0 PAR/SER None 3 3
|
|
// Pin J1-14 PORTB3 6 hSync SER_PULSE 10 CLOCK 11 SRCLK 12 RCLK 4 4
|
|
// Pin J1-13 PORTB2 None None None None None 2
|
|
|
|
// SER_DATA
|
|
// |
|
|
// O O O O O O
|
|
// --------------------+
|
|
// 13 |
|
|
// Arduino +-------+
|
|
// Nano | |
|
|
// / \ | |
|
|
// / \ | USB |
|
|
// \ / | |
|
|
// \ / | |
|
|
// +-------+
|
|
// 8 9 10 11 12 |
|
|
// ----------------------+
|
|
// O O O O O O
|
|
// | | | | |
|
|
// CTL_LATCH | | | SER_LATCH
|
|
// CTL_PULSE | SER_PULSE
|
|
// CTL_DATA
|
|
|
|
#if defined(ARDUINO_AVR_NANO)
|
|
#define platform "ArduinoNano"
|
|
#define maxStorage 30720
|
|
|
|
// Pinout reference:
|
|
// http://lab.dejaworks.com/wp-content/uploads/2016/08/Arduino-Nano-1024x500.png
|
|
// Note that pin 11 and 12 are wrong on some versions of these diagrams
|
|
|
|
// Pins for Gigatron (must be on PORTB)
|
|
#define gigatronDataPin 13 // PB5
|
|
#define gigatronLatchPin 12 // PB4
|
|
#define gigatronPulsePin 11 // PB3
|
|
#define gigatronPinToBitMask digitalPinToBitMask // Regular Arduino pin numbers
|
|
|
|
// Pins for Controller
|
|
#define gameControllerDataPin 10
|
|
#define gameControllerPulsePin 9
|
|
#define gameControllerLatchPin 8
|
|
|
|
// Pins for PS/2 keyboard
|
|
#define keyboardClockPin 3 // Pin 2 or 3 for IRQ
|
|
#define keyboardDataPin 4 // Any available free pin
|
|
|
|
// Link to PC/laptop
|
|
#define hasSerial 1
|
|
|
|
// SD Card
|
|
#define sdChipSelectPin -1
|
|
#endif
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Arduino Micro config |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// See also: https://forum.gigatron.io/viewtopic.php?f=4&t=33
|
|
|
|
// Arduino AVR Gigatron Schematic Controller PCB Gigatron
|
|
// Micro Name OUT bit CD4021 74HC595 (U39) DB9 (J4)
|
|
// ------- ------ -------- --------- ---------- ---------------- --------
|
|
// SCLK PORTB1 None SER_DATA 11 SER INP 14 SER 2
|
|
// MISO PORTB3 7 vSync SER_LATCH 0 PAR/SER None 3
|
|
// MOSI PORTB2 6 hSync SER_PULSE 10 CLOCK 11 SRCLK 12 RCLK 4
|
|
|
|
// SER_LATCH SER_DATA
|
|
// | |
|
|
// O O O O O O
|
|
// --------------------+
|
|
// Reset |
|
|
// Arduino +---+ |
|
|
// Micro | O | |
|
|
// +---+ |
|
|
// |
|
|
// 2 4 6 |
|
|
// ICSP-> o o o |
|
|
// Port .o o o |
|
|
// 1 3 5 |
|
|
// --------------------+
|
|
// O O O O O O
|
|
// |
|
|
// SER_PULSE
|
|
|
|
#if defined(ARDUINO_AVR_MICRO)
|
|
// WattSekunde's setup
|
|
#define platform "ArduinoMicro"
|
|
#define maxStorage 28672
|
|
|
|
// Pinout reference:
|
|
// http://1.bp.blogspot.com/-xqhL0OrJcxo/VJhVxUabhCI/AAAAAAABEVk/loDafkdqLxM/s1600/micro_pinout.png
|
|
|
|
// Pins for Gigatron (must be on PORTB)
|
|
#define gigatronDataPin PB1
|
|
#define gigatronLatchPin PB3
|
|
#define gigatronPulsePin PB2
|
|
// These are not regular Arduino pin numbers
|
|
#define gigatronPinToBitMask(pin) (1 << (pin))
|
|
|
|
// Pins for Controller
|
|
#define gameControllerDataPin -1
|
|
|
|
// Pins for PS/2 keyboard
|
|
#define keyboardClockPin 3 // Pin 2 or 3 for IRQ
|
|
#define keyboardDataPin 4 // Any available free pin
|
|
|
|
// Link to PC/laptop
|
|
#define hasSerial 1
|
|
|
|
// SD Card
|
|
#define sdChipSelectPin -1
|
|
#endif
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| ATtiny85 config |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// Used settings in Arduino IDE 1.8.1:
|
|
// Board: ATtiny
|
|
// Processor: ATtiny85
|
|
// Clock: 8 MHz (internal) --> Use "Burn Bootloader" to configure this
|
|
// Programmer: Arduino as ISP
|
|
// See also:
|
|
// https://create.arduino.cc/projecthub/arjun/programming-attiny85-with-arduino-uno-afb829
|
|
|
|
// +------+
|
|
// ~RESET --|1. 8|-- Vcc
|
|
// PS/2 data PB3 --|2 7|-- PB2 Serial data out
|
|
// PS/2 clock PB4 --|3 6|-- PB1 Serial latch in
|
|
// GND --|4 5|-- PB0 Serial pulse in
|
|
// +------+
|
|
// ATtiny85
|
|
|
|
#if defined(ARDUINO_attiny)
|
|
#define platform "ATtiny85"
|
|
#define maxStorage 8192
|
|
|
|
// Pins for Gigatron (must be on PORTB)
|
|
#define gigatronDataPin PB2
|
|
#define gigatronLatchPin PB1
|
|
#define gigatronPulsePin PB0
|
|
// These are not regular Arduino pin numbers
|
|
#define gigatronPinToBitMask(pin) (1 << (pin))
|
|
|
|
// Pins for Controller
|
|
#define gameControllerDataPin -1
|
|
|
|
// Pins for PS/2 keyboard
|
|
#define keyboardClockPin PB4
|
|
#define keyboardDataPin PB3
|
|
|
|
// Link to PC/laptop
|
|
#define hasSerial 0
|
|
|
|
// SD Card
|
|
#define sdChipSelectPin -1
|
|
#endif
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Pro Micro config |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// Arduino AVR Gigatron Schematic Controller PCB Gigatron
|
|
// Pro Micro Name OUT bit CD4021 74HC595 (U39) DB9 (J4)
|
|
// ---------- ------ -------- --------- ---------- ---------------- --------
|
|
// Pin 13 PORTB5 None SER_DATA 11 SER INP 14 SER 2
|
|
// Pin 12 PORTB4 7 vSync SER_LATCH 0 PAR/SER None 3
|
|
// Pin 11 PORTB3 6 hSync SER_PULSE 10 CLOCK 11 SRCLK 12 RCLK 4
|
|
|
|
#if defined(ARDUINO_AVR_PROMICRO)
|
|
#define platform "ArduinoProMicro"
|
|
#define maxStorage 9999
|
|
|
|
// Pins for Gigatron (must be on PORTB)
|
|
#define gigatronDataPin 8
|
|
#define gigatronLatchPin 9
|
|
#define gigatronPulsePin 10
|
|
#define gigatronPinToBitMask digitalPinToBitMask
|
|
|
|
// Pins for Controller
|
|
#define gameControllerDataPin 5
|
|
#define gameControllerLatchPin 6
|
|
#define gameControllerPulsePin 7
|
|
|
|
// Pins for PS/2 keyboard
|
|
#define keyboardClockPin 3 // Pin 2 or 3 for IRQ
|
|
#define keyboardDataPin 4 // Any available free pin
|
|
|
|
// Link to PC/laptop
|
|
#define hasSerial 1
|
|
|
|
// SD Card
|
|
#define sdChipSelectPin 2
|
|
#endif
|
|
|
|
void (*resetFunc)(void) = 0;
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
| Built-in GT1 images |
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
const byte WozMon_gt1[] PROGMEM = {
|
|
#include "WozMon.h"
|
|
};
|
|
const byte Terminal_gt1[] PROGMEM = {
|
|
#include "Terminal.h"
|
|
};
|
|
const byte Blinky_gt1[] PROGMEM = {
|
|
#include "Blinky.h"
|
|
};
|
|
const struct { const byte *gt1; const char *name; } gt1Files[] =
|
|
{
|
|
{WozMon_gt1, "WozMon" }, // 595 bytes
|
|
{Terminal_gt1, "Terminal" }, // 256 bytes
|
|
{Blinky_gt1, "Blinky" }, // 17 bytes
|
|
{NULL, "-SAVED-" }, // From EEPROM, not PROGMEM
|
|
};
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
| End config section |
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
/*
|
|
* Bit masks for pins
|
|
*/
|
|
byte gigatronDataBit;
|
|
byte gigatronLatchBit;
|
|
byte gigatronPulseBit;
|
|
|
|
/*
|
|
* Loader protocol
|
|
*/
|
|
#define N 60 // Payload bytes per transmission frame
|
|
byte checksum; // Global is simplest
|
|
byte outBuffer[256]; // sendFrame() will read up to index 299 but that's ok.
|
|
// outBuffer[] is global, because having it on the stack
|
|
// can cause trouble on the ATtiny85 (not fully clear why)
|
|
/*
|
|
* Game controller button mapping
|
|
*/
|
|
#define buttonRight 1
|
|
#define buttonLeft 2
|
|
#define buttonDown 4
|
|
#define buttonUp 8
|
|
#define buttonStart 16
|
|
#define buttonSelect 32
|
|
#define buttonB 64
|
|
#define buttonA 128
|
|
// Note: The kit's game controller gives inverted signals.
|
|
|
|
/*
|
|
* Font data
|
|
*/
|
|
const int tinyfont[96] PROGMEM =
|
|
{
|
|
#include "tinyfont.h"
|
|
};
|
|
|
|
/*
|
|
* Terminal mode for upstream host
|
|
*/
|
|
static bool echo = false;
|
|
|
|
/*
|
|
* Non-volatile memory
|
|
*/
|
|
#include <EEPROM.h>
|
|
|
|
struct EEPROMlayout
|
|
{
|
|
byte keymapIndex;
|
|
byte savedFile[];
|
|
};
|
|
|
|
#define fileStart offsetof(struct EEPROMlayout, savedFile)
|
|
static word saveIndex = fileStart; // Write pointer into EEPROM for file (BASIC)
|
|
static word EEPROM_length;
|
|
|
|
#define arrayLen(a) ((int) (sizeof(a) / sizeof((a)[0])))
|
|
extern const byte nrKeymaps; // From in PS2.ino
|
|
|
|
|
|
// Current location for Gt1 transfer from internal storage
|
|
byte *gt1ProgmemLoc;
|
|
|
|
#if sdChipSelectPin >= 0
|
|
// SD card libraries
|
|
#include <SPI.h>
|
|
#include <SD.h>
|
|
|
|
// Current file to be transfered from SD card
|
|
File rootSD;
|
|
String pathSD = "";
|
|
byte dirDepthSD = 0;
|
|
bool validSD = false;
|
|
bool createBackDirSD = false;
|
|
File transferFileSD;
|
|
#endif
|
|
|
|
|
|
/*
|
|
* Setup runs once when the Arduino wakes up
|
|
*/
|
|
void setup()
|
|
{
|
|
gigatronDataBit = gigatronPinToBitMask(gigatronDataPin);
|
|
gigatronLatchBit = gigatronPinToBitMask(gigatronLatchPin);
|
|
gigatronPulseBit = gigatronPinToBitMask(gigatronPulsePin);
|
|
|
|
// Enable output pin (pins are set to input by default)
|
|
PORTB |= gigatronDataBit; // Send 1 when idle
|
|
DDRB = gigatronDataBit;
|
|
|
|
// Set pin modes for game controller passthrough
|
|
#if gameControllerDataPin >= 0
|
|
pinMode(gameControllerDataPin, INPUT_PULLUP); // Force HIGH if disconnected
|
|
pinMode(gameControllerLatchPin, OUTPUT);
|
|
pinMode(gameControllerPulsePin, OUTPUT);
|
|
#endif
|
|
|
|
// Open upstream communication
|
|
#if hasSerial
|
|
Serial.begin(115200);
|
|
//doVersion();
|
|
#endif
|
|
|
|
// Cache for speed
|
|
EEPROM_length = EEPROM.length();
|
|
|
|
// In case we power on together with the Gigatron, this is a
|
|
// good pause to wait for the video loop to have started
|
|
delay(350);
|
|
|
|
// Note that it takes 500~750 ms for PS/2 keyboards to boot
|
|
keyboard_setup();
|
|
|
|
prompt();
|
|
}
|
|
|
|
/*
|
|
* Loop runs repeatedly
|
|
*/
|
|
void loop()
|
|
{
|
|
enum CmdSdCard {CmdSDEnd=0, CmdSDList=1, CmdSDExec=2, CmdSDOpen=3, CmdSDClose=4, CmdSDBegin=5, CmdSDInit=6, NumCmdSD};
|
|
|
|
static bool hasChars = false; // keeps track of partial lines
|
|
static bool cmdSDComm = false; // blocks all input from affecting SDCard comms
|
|
static bool cmdSDMulti = false; // Some SDCard comms commands have multiple bytes with a 0 terminator
|
|
static byte cmdSDCard = CmdSDEnd; // valid SDCard comms command
|
|
|
|
// Check Gigatron's vPulse for incoming data
|
|
int inByte = vSyncByte();
|
|
|
|
// Idle, be robust against partial lines
|
|
if(inByte == -1)
|
|
{
|
|
hasChars = false;
|
|
cmdSDMulti = false;
|
|
cmdSDCard = CmdSDEnd;
|
|
}
|
|
// Building byte so stop all input and comms
|
|
else if(inByte < -1)
|
|
{
|
|
#if 0
|
|
Serial.print(inByte);
|
|
#endif
|
|
return;
|
|
}
|
|
// Valid byte received
|
|
else
|
|
{
|
|
#if 0
|
|
Serial.print(F(" "));
|
|
Serial.print(dirDepthSD);
|
|
Serial.print(cmdSDMulti);
|
|
Serial.print(cmdSDComm);
|
|
Serial.print(validSD);
|
|
Serial.print(rootSD);
|
|
Serial.print(F(" "));
|
|
Serial.print(pathSD);
|
|
Serial.print(F(" "));
|
|
Serial.print(inByte);
|
|
Serial.println();
|
|
Serial.println();
|
|
#endif
|
|
|
|
// Some commands are terminated by a zero trailer
|
|
if(cmdSDMulti || inByte < NumCmdSD)
|
|
{
|
|
static char filename[13] = "", *namePtr = filename;
|
|
static char filepath[13] = "", *pathPtr = filepath;
|
|
|
|
// Valid command
|
|
if(!cmdSDMulti && inByte >= CmdSDList) cmdSDCard = inByte;
|
|
|
|
// If a command is active
|
|
switch(cmdSDCard)
|
|
{
|
|
// Send 63 bytes worth of directory to Gigatron
|
|
case CmdSDList:
|
|
{
|
|
//Serial.print(F("."));
|
|
cmdSDCard = CmdSDEnd;
|
|
doSDDirPayload();
|
|
}
|
|
break;
|
|
|
|
// Execute file on Gigatron
|
|
case CmdSDExec:
|
|
{
|
|
// Multi-byte command
|
|
cmdSDMulti = true;
|
|
|
|
// Build file name
|
|
if(inByte >= 32 && inByte < 127)
|
|
{
|
|
*namePtr++ = inByte;
|
|
}
|
|
// Found terminator so process command
|
|
else if(inByte == CmdSDEnd)
|
|
{
|
|
//Serial.print(F("Execute: "));
|
|
cmdSDComm = false;
|
|
cmdSDMulti = false;
|
|
cmdSDCard = CmdSDEnd;
|
|
|
|
// Need to delay to make sure Gigatron's ROM loader is active
|
|
delay(150);
|
|
|
|
// Terminate string and reset pointer
|
|
*namePtr = 0;
|
|
namePtr = filename;
|
|
|
|
// Files in root and sub dirs are treated differently
|
|
String pathFile = (dirDepthSD == 0) ? pathSD + filename : pathSD + "/" + filename;
|
|
//Serial.println(pathFile);
|
|
doSDFileTransfer((char*)pathFile.c_str(), false);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Open SDCard
|
|
case CmdSDOpen:
|
|
{
|
|
// Multi-byte command
|
|
cmdSDMulti = true;
|
|
|
|
// Build path name
|
|
if(inByte >= 32 && inByte < 127)
|
|
{
|
|
*pathPtr++ = inByte;
|
|
}
|
|
// Found terminator so process command
|
|
else if(inByte == CmdSDEnd)
|
|
{
|
|
//Serial.print(F("Open SDCard: "));
|
|
cmdSDMulti = false;
|
|
cmdSDCard = CmdSDEnd;
|
|
|
|
// Terminate string and reset pointer
|
|
*pathPtr = 0;
|
|
pathPtr = filepath;
|
|
|
|
// Found a sub dir
|
|
if(strlen(filepath))
|
|
{
|
|
// Has a parent directory, (is a sub directory)
|
|
createBackDirSD = true;
|
|
|
|
// Selected parent directory
|
|
if(strcmp(filepath, "..") == 0)
|
|
{
|
|
// Root and sub dirs treat '/' differently
|
|
unsigned int slash = pathSD.lastIndexOf('/');
|
|
if(slash >= 0)
|
|
{
|
|
// Root and sub dirs treat '/' differently
|
|
(dirDepthSD <= 1) ? pathSD.remove(slash + 1) : pathSD.remove(slash);
|
|
dirDepthSD --;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Root and sub dirs treat '/' differently
|
|
dirDepthSD++;
|
|
pathSD = (dirDepthSD <= 1) ? pathSD + filepath : pathSD + "/" + filepath;
|
|
}
|
|
|
|
strcpy(filepath, "");
|
|
}
|
|
// Root dir
|
|
else
|
|
{
|
|
dirDepthSD = 0;
|
|
createBackDirSD = false;
|
|
pathSD = "/";
|
|
}
|
|
|
|
// Open dir
|
|
if(validSD) rootSD = SD.open(pathSD);
|
|
if(!validSD || !rootSD)
|
|
{
|
|
dirDepthSD = 0;
|
|
createBackDirSD = false;
|
|
pathSD = "";
|
|
}
|
|
|
|
// Tell the vCPU browser that we are ready
|
|
wakeBrowser();
|
|
//Serial.println(pathSD);
|
|
}
|
|
}
|
|
break;
|
|
|
|
// Close SDCard
|
|
case CmdSDClose:
|
|
{
|
|
//Serial.println(F("\nClose SDCard:"));
|
|
cmdSDComm = false;
|
|
cmdSDCard = CmdSDEnd;
|
|
|
|
if(validSD && rootSD) rootSD.close();
|
|
}
|
|
break;
|
|
|
|
// Begin SDCard
|
|
case CmdSDBegin:
|
|
{
|
|
cmdSDCard = CmdSDEnd;
|
|
validSD = SD.begin(sdChipSelectPin);
|
|
|
|
// Tell the vCPU browser that we are ready
|
|
wakeBrowser();
|
|
}
|
|
break;
|
|
|
|
// Init SDCard comms
|
|
case CmdSDInit:
|
|
{
|
|
cmdSDComm = true;
|
|
cmdSDCard = CmdSDEnd;
|
|
}
|
|
break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
// TinyBASIC EEPROM saving
|
|
else
|
|
{
|
|
if(saveIndex < EEPROM_length) // Store byte in EEPROM if possible
|
|
EEPROM.write(saveIndex++, inByte);
|
|
else if(hasChars) // Full, but only break if line is non-empty
|
|
sendController(3, 10); // Send long Ctrl-C back to stop sender
|
|
if(inByte >= 32)
|
|
hasChars = true; // Mark printable characters as non-empty
|
|
else if(inByte == '\n')
|
|
{
|
|
if(!hasChars) // Empty lines delete the old program
|
|
saveIndex = fileStart;
|
|
if(saveIndex < EEPROM_length) // EOF terminator
|
|
EEPROM.write(saveIndex, 255);
|
|
hasChars = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip all input during SDCard comms, (from start to close)
|
|
if(cmdSDComm) return;
|
|
|
|
// Game controller pass through (Courtesy norgate)
|
|
#if gameControllerDataPin >= 0
|
|
for(;;)
|
|
{
|
|
byte serialByte = 0;
|
|
sendPulse(gameControllerLatchPin);
|
|
for(byte i = 0; i < 8; i++) // Shift in all 8 bits
|
|
{
|
|
serialByte <<= 1;
|
|
serialByte |= digitalRead(gameControllerDataPin);
|
|
sendPulse(gameControllerPulsePin);
|
|
}
|
|
if(serialByte == 255) // Skip if no button pressed
|
|
break;
|
|
if(!sendController(serialByte, 1)) // Forward byte to Gigatron
|
|
{
|
|
cmdSDComm = true;
|
|
return; // Received data from gigatron so bail
|
|
}
|
|
} // Loop locally while active to skip PS/2 and waitVSync
|
|
|
|
// Allow PS/2 interrupts for a reasonable window
|
|
delay(14); // The game controller probe takes 1 ms
|
|
#else
|
|
delay(15);
|
|
#endif
|
|
|
|
// PS/2 keyboard events
|
|
byte key = keyboard_getState();
|
|
if(key != 255)
|
|
{
|
|
byte f = fnKey(key ^ 64); // Ctrl+Fn key?
|
|
if(f)
|
|
{
|
|
if(f == 1)
|
|
doMapping(); // Ctrl-F1 is help
|
|
else if(f - 2 < arrayLen(gt1Files))
|
|
{
|
|
if(gt1Files[f - 2].gt1)
|
|
{
|
|
gt1ProgmemLoc = (byte*)gt1Files[f - 2].gt1; // Set Location of built-in GT1 file
|
|
doTransfer(readNextProgmem, NULL); // Send GT1 file to Gigatron
|
|
}
|
|
else
|
|
sendSavedFile();
|
|
}
|
|
}
|
|
for(;;) // Focus all attention on PS/2 until state is idle again
|
|
{
|
|
if(!fnKey(key ^ 64)) // Filter away the Ctrl+Fn combinations here
|
|
{
|
|
critical();
|
|
sendFirstByte(key); // Synchronize with vPulse and send ASCII code
|
|
nonCritical();
|
|
}
|
|
if(key == 255) // Break after returning to the idle state
|
|
break;
|
|
delay(15); // Allow PS/2 interrupts, so we can receive break codes
|
|
key = keyboard_getState(); // This typically returns the same key for a couple of frames
|
|
}
|
|
}
|
|
|
|
// Commands from upstream USB (PC/laptop)
|
|
#if hasSerial
|
|
#define lineBuffer outBuffer
|
|
static char next = 0, last;
|
|
static byte lineIndex = 0;
|
|
while(Serial.available())
|
|
{
|
|
last = next;
|
|
char next = nextSerial();
|
|
sendEcho(next, last);
|
|
lineBuffer[lineIndex++] = next;
|
|
if(next == '\r' || next == '\n')
|
|
{
|
|
lineBuffer[lineIndex - 1] = '\0';
|
|
doCommand((char*)lineBuffer);
|
|
lineIndex = 0;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool wakeBrowser()
|
|
{
|
|
critical();
|
|
bool result = sendFirstByte(0);
|
|
PORTB |= gigatronDataBit; // send 1 when idle
|
|
nonCritical();
|
|
|
|
return result;
|
|
}
|
|
|
|
void prompt()
|
|
{
|
|
#if hasSerial
|
|
Serial.println(detectGigatron() ? F(":Gigatron OK") : F("!Gigatron offline"));
|
|
Serial.println(F("Cmd?"));
|
|
#endif
|
|
}
|
|
|
|
bool detectGigatron()
|
|
{
|
|
unsigned long timeout = millis() + 85;
|
|
long T[4] = { 0, 0, 0, 0 };
|
|
|
|
// Sample the sync signals coming out of the controller port
|
|
while(millis() < timeout)
|
|
{
|
|
byte pinb = PINB; // capture SER_PULSE and SER_LATCH at the same time
|
|
T[(pinb & gigatronLatchBit ? 2 : 0) + (pinb & gigatronPulseBit ? 1 : 0)]++;
|
|
}
|
|
|
|
float S = T[0] + T[1] + T[2] + T[3] + .1; // Avoid zero division (pedantic)
|
|
float vSync = (T[0] + T[1]) / (8 * S / 521); // Adjusted vSync signal
|
|
float hSync = (T[0] + T[2]) / (96 * S / 800); // Standard hSync signal
|
|
|
|
// Check that vSync and hSync characteristics look normal
|
|
return 0.95 <= vSync && vSync <= 1.25 && 0.90 <= hSync && hSync <= 1.10;
|
|
}
|
|
|
|
void sendEcho(char next, char last)
|
|
{
|
|
#if hasSerial
|
|
if(echo)
|
|
switch(next)
|
|
{
|
|
case 127: Serial.print(F("\b \b")); break;
|
|
|
|
// !!! FALL THROUGH !!!
|
|
case '\n': if(last == '\r') break;
|
|
case '\r': Serial.println(); break;
|
|
|
|
default: Serial.print(next);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void doCommand(char line[])
|
|
{
|
|
int arg = line[0] ? atoi(&line[1]) : 0;
|
|
switch(toupper(line[0]))
|
|
{
|
|
case 'V': doVersion(); break;
|
|
case 'H': doHelp(); break;
|
|
case 'R': doReset(arg); break;
|
|
case 'L': doLoader(); break;
|
|
case 'M': doMapping(); break;
|
|
case 'P': doProgmemFileTransfer(arg); break;
|
|
case 'U': doTransfer(readNextSerial, askSerial); break;
|
|
case 'K': doSDFileTransfer(&line[1], true); break;
|
|
case 'J': doPrintSDFiles(); break;
|
|
case '.': doLine(&line[1]); break;
|
|
case 'C': doEcho(!echo); break;
|
|
case 'T': doTerminal(); break;
|
|
case 'W': sendController(~buttonUp, 2); break;
|
|
case 'A': sendController(~buttonLeft, 2); break;
|
|
case 'S': sendController(~buttonDown, 2); break;
|
|
case 'D': sendController(~buttonRight, 2); break;
|
|
case 'Z': sendController(~buttonA & 255, 2); break;
|
|
case 'X': sendController(~buttonB, 2); break;
|
|
case 'Q': sendController(~buttonSelect, 2); break;
|
|
case 'E': sendController(~buttonStart, 2); break;
|
|
case 0: /* Empty line */ break;
|
|
|
|
default:
|
|
#if hasSerial
|
|
Serial.println(F("!Unknown command (type 'H' for help)"));
|
|
#endif
|
|
break;
|
|
}
|
|
prompt();
|
|
}
|
|
|
|
void doVersion()
|
|
{
|
|
#if hasSerial
|
|
Serial.println(F(":BabelFish platform=" platform));
|
|
Serial.println(F(":Pins:"));
|
|
#define V(s) #s
|
|
#define Q(s) V(s)
|
|
Serial.println(F(": Gigatron data=" Q(gigatronDataPin) " latch=" Q(gigatronLatchPin) " pulse=" Q(gigatronPulsePin)));
|
|
Serial.println(F(": Keyboard clock=" Q(keyboardClockPin) " data=" Q(keyboardDataPin)));
|
|
#if gameControllerDataPin >= 0
|
|
Serial.println(F(": Controller data=" Q(gameControllerDataPin) " latch=" Q(gameControllerLatchPin) " pulse=" Q(gameControllerPulsePin)));
|
|
#endif
|
|
|
|
Serial.println(F(":EEPROM:"));
|
|
Serial.print(F(": size="));
|
|
Serial.print(EEPROM.length());
|
|
Serial.print(F(" mapping="));
|
|
Serial.println(getKeymapName());
|
|
Serial.println(F(":PROGMEM slots:"));
|
|
for(byte i = 0; i < arrayLen(gt1Files); i++)
|
|
{
|
|
Serial.print(F(": P"));
|
|
Serial.print(i);
|
|
Serial.print(F(") "));
|
|
Serial.println(gt1Files[i].name);
|
|
}
|
|
doEcho(echo);
|
|
Serial.println(F(":Type 'H' for help"));
|
|
#endif
|
|
}
|
|
|
|
// Show keymapping in Loader screen (Loader must be running)
|
|
void doMapping()
|
|
{
|
|
word pos = 0x800;
|
|
char text[] = "Ctrl-F1 This help";
|
|
// 0123456789
|
|
pos = renderLine(pos, text);
|
|
text[9] = 0;
|
|
for(byte i = 0; i < arrayLen(gt1Files); i++)
|
|
{
|
|
byte f = i + 2;
|
|
// To save space avoid itoa() or sprintf()
|
|
text[6] = '0' + f / 10;
|
|
text[7] = ' ';
|
|
text[6 + f / 10] = '0' + f % 10;
|
|
pos = renderString(pos, text);
|
|
pos = renderLine(pos, gt1Files[i].name);
|
|
}
|
|
pos = renderString(pos, "Keymap: ");
|
|
pos = renderString(pos, getKeymapName());
|
|
pos = renderLine(pos, " (Change with Ctrl-Alt-Fxx)");
|
|
pos = renderString(pos, "Available:");
|
|
for(byte i = 0; i < nrKeymaps; i++)
|
|
{
|
|
pos = renderString(pos, " ");
|
|
pos = renderString(pos, getKeymapName(i));
|
|
}
|
|
}
|
|
|
|
void doEcho(byte value)
|
|
{
|
|
#if hasSerial
|
|
echo = value;
|
|
Serial.print(F(":Echo "));
|
|
Serial.println(value ? F("on") : F("off"));
|
|
#endif
|
|
}
|
|
|
|
void doHelp()
|
|
{
|
|
#if hasSerial
|
|
Serial.println(F(":Commands are"));
|
|
Serial.println(F(": V Show configuration"));
|
|
Serial.println(F(": H Show this help"));
|
|
Serial.println(F(": R Reset Gigatron"));
|
|
Serial.println(F(": L Start Loader"));
|
|
Serial.println(F(": M Show key mapping or menu in Loader screen"));
|
|
Serial.println(F(": P[<n>] Transfer object file from PROGMEM slot <n>"));
|
|
Serial.print(F(": P")); Serial.print(arrayLen(gt1Files) - 1);
|
|
Serial.println(F(" Type saved EEPROM data back into Gigatron"));
|
|
Serial.println(F(": [Hint: Use '.SAVE' for saving, not 'T'-mode!]"));
|
|
Serial.println(F(": U Transfer object file from USB"));
|
|
Serial.println(F(": K<name> Transfer from SD"));
|
|
Serial.println(F(": J List SD"));
|
|
Serial.println(F(": .<text> Send text line as ASCII keystrokes"));
|
|
Serial.println(F(": C Toggle echo mode (default off)"));
|
|
Serial.println(F(": T Enter terminal mode"));
|
|
Serial.println(F(": W/A/S/D Up/left/down/right arrow"));
|
|
Serial.println(F(": Z/X A/B button"));
|
|
Serial.println(F(": Q/E Select/start button"));
|
|
#endif
|
|
}
|
|
|
|
void doReset(int n)
|
|
{
|
|
// Soft reset: hold start for >128 frames (>2.1 seconds)
|
|
#if hasSerial
|
|
Serial.println(F(":Resetting Gigatron"));
|
|
Serial.flush();
|
|
#endif
|
|
sendController(~buttonStart, n ? n : 150);
|
|
|
|
// Wait for main menu to be ready
|
|
delay(1500);
|
|
}
|
|
|
|
void doLoader()
|
|
{
|
|
// Navigate menu. 'Loader' is at the bottom
|
|
#if hasSerial
|
|
Serial.println(F(":Starting Loader from menu"));
|
|
Serial.flush();
|
|
#endif
|
|
|
|
for(byte i = 0; i < 10; i++)
|
|
{
|
|
sendController(~buttonDown, 2);
|
|
delay(50);
|
|
}
|
|
|
|
// Start 'Loader' application on Gigatron
|
|
sendController(~buttonA & 255, 2);
|
|
|
|
// Wait for Loader to be running
|
|
delay(1000);
|
|
}
|
|
|
|
void doLine(char *line)
|
|
{
|
|
// Pass through the line of text
|
|
for(byte i = 0; line[i]; i++)
|
|
{
|
|
sendController(line[i], 2);
|
|
delay(20); // Allow Gigatron software to process key code
|
|
}
|
|
// And terminal with a CR
|
|
sendController('\n', 2);
|
|
delay(50); // Allow Gigatron software to process line
|
|
}
|
|
|
|
// Send list of decimal numbers as byte stream
|
|
void doBytes(char *line)
|
|
{
|
|
do
|
|
{
|
|
if(*line >= '0')
|
|
{
|
|
byte b = 0;
|
|
do
|
|
b = (10 * b) + (*line++ - '0');
|
|
while(*line >= '0');
|
|
sendController(b, 1);
|
|
}
|
|
}
|
|
while(*line++ != '\0');
|
|
}
|
|
|
|
// In terminal mode we transfer every incoming character to
|
|
// the Gigatron, with some substitutions for convenience.
|
|
// This lets you type directly into BASIC and WozMon from
|
|
// a terminal window on your PC or laptop.
|
|
//
|
|
// picomon -b 115200 /dev/tty.usbmodem1411
|
|
// screen /dev/tty.usbmodem1411 115200
|
|
|
|
void doTerminal()
|
|
{
|
|
#if hasSerial
|
|
Serial.println(F(":Entering terminal mode"));
|
|
Serial.println(F(":Exit with Ctrl-D"));
|
|
char next = 0, last;
|
|
bool ansi = false;
|
|
for(;;)
|
|
{
|
|
if(Serial.available())
|
|
{
|
|
byte out;
|
|
last = next;
|
|
next = nextSerial();
|
|
sendEcho(next, last);
|
|
|
|
// Mappings for newline and arrow sequences
|
|
out = next;
|
|
switch(next)
|
|
{
|
|
case 4: return; // Ctrl-D (EOT)
|
|
case 9: out = ~buttonB; break; // Same as PS/2 above
|
|
case '\r': out = '\n'; break; // Treat as \n
|
|
case '\n': if(last == '\r') continue; break; // Swallow if after \r
|
|
case '\e': continue; // ANSI escape sequence
|
|
case '[': if(last == '\e') ansi = true; continue;
|
|
case 'A': if(ansi) out = ~buttonUp; break; // Map cursor keys to buttons
|
|
case 'B': if(ansi) out = ~buttonDown; break;
|
|
case 'C': if(ansi) out = ~buttonRight; break;
|
|
case 'D': if(ansi) out = ~buttonLeft; break;
|
|
}
|
|
|
|
sendController(out, 2);
|
|
ansi = false;
|
|
}
|
|
else
|
|
{
|
|
|
|
// If we receive data in terminal mode, forward it all
|
|
// to the the host (instead of storing it into EEPROM)
|
|
int inByte = vSyncByte(); // Check for data carried with /vSync
|
|
if(inByte >= 0)
|
|
{
|
|
if(inByte == 10)
|
|
Serial.print('\r');
|
|
Serial.print((char)inByte);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void doProgmemFileTransfer(int arg)
|
|
{
|
|
#if hasSerial
|
|
Serial.println(F(":Sending from PROGMEM"));
|
|
#endif
|
|
if(0 <= arg && arg < arrayLen(gt1Files))
|
|
{
|
|
gt1ProgmemLoc = (byte*)gt1Files[arg].gt1; // Set Location of built-in GT1 file
|
|
doTransfer(readNextProgmem, NULL); // Send GT1 file to Gigatron
|
|
}
|
|
}
|
|
|
|
void doSDFileTransfer(char *filename, bool serialEnabled)
|
|
{
|
|
#if hasSerial
|
|
//if(serialEnabled)
|
|
// Serial.println(F(":Sending from SD card"));
|
|
#endif
|
|
#if sdChipSelectPin >= 0
|
|
File dataFile = SD.open(filename);
|
|
if(!dataFile)
|
|
{
|
|
#if hasSerial
|
|
// if(serialEnabled)
|
|
// Serial.println(F("!Not on SD"));
|
|
#endif
|
|
return;
|
|
}
|
|
transferFileSD = dataFile;
|
|
doTransfer(readNextSD, NULL);
|
|
dataFile.close();
|
|
#endif
|
|
}
|
|
|
|
void doSDDirPayload()
|
|
{
|
|
enum EntryType {EntryFile=1, EntryDir=2, EntryError=3};
|
|
|
|
const byte kNameLength = 12;
|
|
static char paths[8][kNameLength + 1];
|
|
|
|
File entry;
|
|
byte isLast = 0;
|
|
byte index=0, payload[N+3];
|
|
|
|
|
|
// Gigatron payload is 63 bytes, protocol is <isLast>, <len>, <payload 0...62>
|
|
// Entry is maximum 15 bytes, <type> <len> <name:12> <0>, where name <= 12
|
|
// Try and fit as many entry packets into the payload as possible before shipping it
|
|
byte packets = 0;
|
|
const char parentDir[] = "..";
|
|
for(;;)
|
|
{
|
|
byte nameSize = 0;
|
|
char *nameEntry = nullptr;
|
|
|
|
// SDCard error, (missing or incorrect format, etc)
|
|
if(!isLast && (!validSD || !rootSD))
|
|
{
|
|
// Add error entry
|
|
isLast = 1;
|
|
nameEntry = "SDCard Error";
|
|
nameSize = strlen(nameEntry);
|
|
payload[index++] = EntryError;
|
|
}
|
|
// Create parent directory entry
|
|
else if(dirDepthSD && createBackDirSD)
|
|
{
|
|
createBackDirSD = false;
|
|
nameEntry = (char *)parentDir;
|
|
nameSize = strlen(nameEntry);
|
|
payload[index++] = EntryDir;
|
|
}
|
|
// Create file/dir entry
|
|
else
|
|
{
|
|
// Check if there is room for one more entry in payload
|
|
if(index + 2 + kNameLength + 1 >= N + 3)
|
|
{
|
|
isLast = 0;
|
|
entry.close();
|
|
break;
|
|
}
|
|
|
|
// Get next entry
|
|
if(validSD && rootSD) entry = rootSD.openNextFile();
|
|
if(!validSD || !rootSD || !entry)
|
|
{
|
|
// This is the last payload
|
|
isLast = 1;
|
|
if(validSD && rootSD) rootSD.rewindDirectory();
|
|
break;
|
|
}
|
|
// Valid entry
|
|
else
|
|
{
|
|
// Entry name and size
|
|
nameEntry = entry.name();
|
|
nameSize = strlen(nameEntry);
|
|
|
|
// File
|
|
if(!entry.isDirectory())
|
|
{
|
|
// Accept .gt1 and .gt1x files
|
|
String name = nameEntry;
|
|
if(name.endsWith(".GT1") || name.endsWith(".GT1X"))
|
|
{
|
|
payload[index++] = EntryFile;
|
|
}
|
|
// Filter everything else
|
|
else
|
|
{
|
|
entry.close();
|
|
continue;
|
|
}
|
|
}
|
|
// Dir
|
|
else
|
|
{
|
|
payload[index++] = EntryDir;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add entry to payload
|
|
payload[index++] = nameSize;
|
|
strcpy((char*)&payload[index], nameEntry);
|
|
index += nameSize;
|
|
payload[index++] = 0;
|
|
entry.close();
|
|
|
|
#if hasSerial
|
|
//Serial.println(nameEntry);
|
|
#endif
|
|
}
|
|
|
|
// Ship payload
|
|
critical();
|
|
sendFirstByte(isLast); // isLast 0, 1
|
|
sendBits(index, 6); // length 0..63
|
|
for(byte i=0; i<N+3; i++) // payload bytes
|
|
{
|
|
sendBits(payload[i], 8);
|
|
}
|
|
PORTB |= gigatronDataBit; // send 1 when idle
|
|
nonCritical();
|
|
}
|
|
|
|
void doPrintSDFiles()
|
|
{
|
|
#if hasSerial and sdChipSelectPin >= 0
|
|
Serial.println(F(":Files:"));
|
|
File root = SD.open("/");
|
|
File current;
|
|
while(current = root.openNextFile())
|
|
{
|
|
if(!current.isDirectory())
|
|
{
|
|
Serial.print(F(": File: "));
|
|
Serial.println(current.name());
|
|
}
|
|
current.close();
|
|
}
|
|
root.close();
|
|
#endif
|
|
}
|
|
|
|
// Render line in Loader screen
|
|
word renderLine(word pos, const char *text)
|
|
{
|
|
pos = renderString(pos, text);
|
|
return (pos & 0xff00) + 0x600; // Goes to new line
|
|
}
|
|
|
|
// Render string in Loader screen
|
|
word renderString(word pos, const char text[])
|
|
{
|
|
// Send 6 pixel lines to Gigatron
|
|
// The ATtiny85 doesn't have sufficient RAM for separate bitmap[] and
|
|
// pixelLine[] arrays. Therefore the rendering must be redone with each
|
|
// iteration, followed by an in-place conversion to pixel colors
|
|
|
|
word p = pos;
|
|
byte x;
|
|
for(byte b = 32; b; b >>= 1)
|
|
{
|
|
// (Re-)render line of text in bitmap
|
|
x = 0;
|
|
for(byte i = 0; text[i] != 0; i++)
|
|
{
|
|
// Get pixel data for character
|
|
int pixels = pgm_read_word(&tinyfont[text[i] - 32]);
|
|
|
|
// Render character in bitmap
|
|
if(pixels >= 0)
|
|
{
|
|
outBuffer[x++] = 0; // Regular position
|
|
outBuffer[x++] = (pixels >> 9) & 62;
|
|
outBuffer[x++] = (pixels >> 4) & 62;
|
|
outBuffer[x++] = (pixels << 1) & 62;
|
|
}
|
|
else
|
|
{
|
|
outBuffer[x++] = 0; // Shift down for g, j, p, q, y
|
|
outBuffer[x++] = (pixels >> 10) & 31;
|
|
outBuffer[x++] = (pixels >> 5) & 31;
|
|
outBuffer[x++] = pixels & 31;
|
|
if(text[i] == 'j') // Special case to dot the j
|
|
outBuffer[x - 1] = '.';
|
|
}
|
|
}
|
|
|
|
// Convert bitmap to pixels
|
|
const byte bgColor = 32; // Blue
|
|
const byte fgColor = 63; // White
|
|
for(byte i = 0; i < x; i++)
|
|
outBuffer[i] = (outBuffer[i] & b) ? fgColor : bgColor;
|
|
|
|
// Send line of pixels to Gigatron
|
|
sendGt1Segment(p, x);
|
|
|
|
// To next scanline
|
|
p += 256;
|
|
}
|
|
|
|
return pos + x;
|
|
}
|
|
|
|
|
|
// read next GT1 byte from USB
|
|
int readNextSerial()
|
|
{
|
|
#if hasSerial
|
|
return nextSerial();
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
// read next GT1 byte from internal memory
|
|
int readNextProgmem()
|
|
{
|
|
return pgm_read_byte(gt1ProgmemLoc++);
|
|
}
|
|
|
|
// read next GT1 byte from SD card
|
|
int readNextSD()
|
|
{
|
|
#if sdChipSelectPin >= 0
|
|
if(transferFileSD.available()) return transferFileSD.read();
|
|
else return -1;
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
// ask for n bytes via serial
|
|
void askSerial(int n)
|
|
{
|
|
#if hasSerial
|
|
Serial.print(n);
|
|
Serial.println(F("?"));
|
|
#endif
|
|
}
|
|
|
|
// Because the Arduino doesn't have enough RAM to buffer
|
|
// a complete GT1 file, it processes these files segment
|
|
// by segment. Each segment is transmitted downstream in
|
|
// concatenated frames to the Gigatron. Between segments
|
|
// it is communicating upstream with the serial port.
|
|
|
|
void doTransfer(int(*readNext)(), void(*ask)(int))
|
|
{
|
|
int nextByte;
|
|
|
|
#if hasSerial
|
|
if(!waitVSync())
|
|
{
|
|
Serial.print(F("!Failed"));
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if(ask)ask(3);
|
|
|
|
nextByte = readNext();
|
|
word address = nextByte;
|
|
|
|
// Any number n of segments (n>0)
|
|
do {
|
|
// Segment start and length
|
|
nextByte = readNext();
|
|
address = (address << 8) + nextByte;
|
|
nextByte = readNext();
|
|
int len = nextByte ? nextByte : 256;
|
|
|
|
if(ask)ask(len);
|
|
|
|
// Copy data into send buffer
|
|
for(int i = 0; i < len; i++)
|
|
{
|
|
nextByte = readNext();
|
|
outBuffer[i] = nextByte;
|
|
}
|
|
|
|
// Check that segment doesn't cross the page boundary
|
|
if((address & 255) + len > 256)
|
|
{
|
|
#if hasSerial
|
|
// Serial.println(F("!Data error (page overflow)"));
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// Send downstream
|
|
#if hasSerial
|
|
//Serial.print(F(":Loading "));
|
|
//Serial.print(len);
|
|
//Serial.print(F(" bytes at $"));
|
|
//Serial.println(address, HEX);
|
|
//Serial.flush();
|
|
#endif
|
|
sendGt1Segment(address, len);
|
|
|
|
// Signal that we're ready to receive more
|
|
if(ask)ask(3);
|
|
nextByte = readNext();
|
|
address = nextByte;
|
|
|
|
} while(address != 0);
|
|
|
|
// Two bytes for start address
|
|
nextByte = readNext();
|
|
|
|
address = nextByte;
|
|
nextByte = readNext();
|
|
address = (address << 8) + nextByte;
|
|
if(address != 0)
|
|
{
|
|
#if hasSerial
|
|
//Serial.print(F(":Executing from $"));
|
|
//Serial.println(address, HEX);
|
|
//Serial.flush();
|
|
#endif
|
|
sendGt1Execute(address, outBuffer + 240);
|
|
}
|
|
}
|
|
|
|
int nextSerial()
|
|
{
|
|
#if hasSerial
|
|
unsigned long timeout = millis() + 5000;
|
|
while(!Serial.available() && millis() < timeout);
|
|
|
|
int nextByte = Serial.read();
|
|
if(nextByte < 0)
|
|
Serial.println(F("!Timeout error (no data)"));
|
|
|
|
// Workaround suspected bug in USB support for ATmega32U4 (Arduino Micro,
|
|
// Leonardo, etcetera) in Arduino's USBCore.cpp. These boards don't have a
|
|
// support processor for USB handling but use an on-chip USB controller
|
|
// and a different software stack for that.
|
|
// From Atmel-7766J-USB-ATmega16U4/32U4-Datasheet_04/2016:
|
|
//
|
|
// "RXOUTI shall always be cleared before clearing FIFOCON."
|
|
//
|
|
// (An identical remark is in the datasheets for ATmega32U6/AT90USB64/128)
|
|
//
|
|
// However:
|
|
// Serial.read() ->
|
|
// CDC.cpp/Serial_::read ->
|
|
// USBCore.cpp/USB_Recv() ->
|
|
// USBCore.cpp/ReleaseRX() ->
|
|
// UEINTX = 0x6B; // FIFOCON=0 NAKINI=1 RWAL=1 NAKOUTI=0 RXSTPI=1 RXOUTI=0 STALLEDI=1 TXINI=1
|
|
//
|
|
// This last statement attempts to clear both bits AT ONCE. This fails to
|
|
// clear FIFOCON when host data arrives in exact multiples of 64,128,192,...
|
|
// bytes and when using double buffering with two banks of bytes, as
|
|
// USBCore.cpp does. A hangup situation occurs after reading the first
|
|
// transmitted 64 bytes. This can then only be solved by resetting the board,
|
|
// because no further host data reaches the sketch.
|
|
//
|
|
// A better fix would be to repair Arduino's USB_Recv and ReleaseRX.
|
|
// See for follow-up https://github.com/arduino/Arduino/issues/7838
|
|
// and https://github.com/kervinck/gigatron-rom/issues/36
|
|
#if defined(USBCON) && defined(UEINTX) && defined(UEBCLX)
|
|
if(!UEBCLX) // If bank empty
|
|
UEINTX &= ~(1 << FIFOCON); // Clear FIFOCON bit
|
|
#endif
|
|
|
|
return nextByte;
|
|
#endif
|
|
}
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
| Gigatron communication |
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
static inline void critical()
|
|
{
|
|
forbidPs2();
|
|
noInterrupts();
|
|
}
|
|
|
|
static inline void nonCritical()
|
|
{
|
|
interrupts();
|
|
allowPs2();
|
|
}
|
|
|
|
// Send a 1..256 byte code or data segment into the Gigatron by
|
|
// repacking it into Loader frames of max N=60 payload bytes each.
|
|
void sendGt1Segment(word address, int len)
|
|
{
|
|
byte n = min(N, len);
|
|
resetChecksum();
|
|
|
|
// Send segment data
|
|
critical();
|
|
for(int i = 0; i < len; i += n)
|
|
{
|
|
n = min(N, len - i);
|
|
sendFrame('L', n, address + i, outBuffer + i);
|
|
}
|
|
nonCritical();
|
|
|
|
// Wait for vPulse to start so we're 100% sure to skip one frame and
|
|
// the checksum resets on the other side. (This is a bit pedantic)
|
|
while(PINB & gigatronLatchBit); // ~160 us
|
|
}
|
|
|
|
// Send execute command
|
|
void sendGt1Execute(word address, byte data[])
|
|
{
|
|
critical();
|
|
resetChecksum();
|
|
sendFrame('L', 0, address, data);
|
|
nonCritical();
|
|
}
|
|
|
|
// Pretend to be a game controller
|
|
// Send the same byte a few frames, just like a human user
|
|
bool sendController(byte value, int n)
|
|
{
|
|
// Send controller code for n frames
|
|
// E.g. 4 frames = 3/60s = ~50 ms
|
|
critical();
|
|
for(int i = 0; i < n; i++)
|
|
{
|
|
if(!sendFirstByte(value))
|
|
{
|
|
nonCritical();
|
|
return false;
|
|
}
|
|
}
|
|
nonCritical();
|
|
|
|
PORTB |= gigatronDataBit; // Send 1 when idle
|
|
|
|
return true;
|
|
}
|
|
|
|
void resetChecksum()
|
|
{
|
|
// Setup checksum properly
|
|
checksum = 'g';
|
|
}
|
|
|
|
void sendFrame(byte firstByte, byte len, word address, byte message[])
|
|
{
|
|
// Send one frame of data
|
|
//
|
|
// A frame has 65*8-2=518 bits, including protocol byte and checksum.
|
|
// The reasons for the two "missing" bits are:
|
|
// 1. From start of vertical pulse, there are 35 scanlines remaining
|
|
// in vertical blank. But we also need the payload bytes to align
|
|
// with scanlines where the interpreter runs, so the Gigatron doesn't
|
|
// have to shift everything it receives by 1 bit.
|
|
// 2. There is a 1 bit latency inside the 74HC595 for the data bit,
|
|
// but (obviously) not for the sync signals.
|
|
// All together, we drop 2 bits from the 2nd byte in a frame. This achieves
|
|
// byte alignment for the Gigatron at visible scanline 3, 11, 19, ... etc.
|
|
|
|
sendFirstByte(firstByte); // Protocol byte
|
|
checksum += firstByte << 6; // Keep Loader.gcl dumb
|
|
sendBits(len, 6); // Length 0, 1..60
|
|
sendBits(address & 255, 8); // Low address bits
|
|
sendBits(address >> 8, 8); // High address bits
|
|
for(byte i = 0; i < N; i++) // Payload bytes
|
|
sendBits(message[i], 8);
|
|
byte lastByte = -checksum; // Checksum must come out as 0
|
|
sendBits(lastByte, 8);
|
|
checksum = lastByte; // Concatenate checksums
|
|
PORTB |= gigatronDataBit; // Send 1 when idle
|
|
}
|
|
|
|
bool sendFirstByte(byte value)
|
|
{
|
|
// Wait vertical sync NEGATIVE edge to sync with loader
|
|
while(~PINB & gigatronLatchBit); // Ensure vSync is HIGH first
|
|
|
|
// Send first bit in advance
|
|
if(value & 128)
|
|
PORTB |= gigatronDataBit;
|
|
else
|
|
PORTB &= ~gigatronDataBit;
|
|
|
|
while(PINB & gigatronLatchBit); // Then wait for vSync to drop
|
|
|
|
// Wait for bit transfer at horizontal sync RISING edge. As this is at
|
|
// the end of a short (3.8 us) pulse following VERY shortly (0.64us) after
|
|
// vSync drop, this timing is tight. That is the reason that interrupts
|
|
// must be disabled on the microcontroller (and why 1 MHz isn't enough).
|
|
while(PINB & gigatronPulseBit); // Ensure hSync is LOW first
|
|
|
|
while(~PINB & gigatronPulseBit); // Then wait for hSync to rise
|
|
|
|
// Send remaining bits
|
|
return sendBits(value, 7);
|
|
}
|
|
|
|
// Send n bits, highest first
|
|
bool sendBits(byte value, byte n)
|
|
{
|
|
byte count = 0;
|
|
for(byte bit=1<<(n-1); bit; bit>>=1)
|
|
{
|
|
// Send next bit
|
|
if(value & bit)
|
|
PORTB |= gigatronDataBit;
|
|
else
|
|
PORTB &= ~gigatronDataBit;
|
|
|
|
// Wait for bit transfer at horizontal sync POSITIVE edge.
|
|
while(PINB & gigatronPulseBit); // Ensure hSync is LOW first
|
|
|
|
if(~PINB & gigatronLatchBit) // While in vPulse count hSync's
|
|
count++;
|
|
while(~PINB & gigatronPulseBit); // Then wait for hSync to rise
|
|
}
|
|
|
|
checksum += value;
|
|
|
|
if(count != n) return false; // Received a zero bit from Gigatron, so bail
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check Gigatron's vPulse for incoming data
|
|
// Return each completed byte value, -1 for idle, < -1 for busy
|
|
int vSyncByte()
|
|
{
|
|
static byte inByte, inBit;
|
|
|
|
critical();
|
|
byte count = waitVSync();
|
|
nonCritical();
|
|
|
|
inByte &= ~inBit; // Clear current bit
|
|
switch(count)
|
|
{
|
|
case 9: inByte |= inBit; // Received a one bit
|
|
// !!! FALL THROUGH !!!
|
|
case 7: // Received a zero bit
|
|
{
|
|
inBit <<= 1;
|
|
if(!inBit)
|
|
{
|
|
inBit = 1; // Prepare for next byte
|
|
return inByte; // Return full byte
|
|
}
|
|
}
|
|
break;
|
|
|
|
default: inBit = 1; // Reset incoming data state
|
|
}
|
|
return -inBit;
|
|
}
|
|
|
|
// Count number of hSync pulses during vPulse
|
|
// This is a way for the Gigatron to send information out
|
|
byte waitVSync()
|
|
{
|
|
word timeout = 0; // 2^16 cycles must give at least 17 ms
|
|
|
|
// Wait vertical sync NEGATIVE edge
|
|
|
|
while(~PINB & gigatronLatchBit) // Ensure vSync is HIGH first
|
|
if(!--timeout)
|
|
return 0;
|
|
|
|
while(PINB & gigatronLatchBit) // Then wait for vSync to drop
|
|
if(!--timeout)
|
|
return 0;
|
|
|
|
// Now count horizontal sync POSITIVE edges
|
|
byte count = 0;
|
|
for(;;)
|
|
{
|
|
while(PINB & gigatronPulseBit); // Ensure hSync is LOW first
|
|
|
|
if(PINB & gigatronLatchBit) // Not in vPulse anymore
|
|
break;
|
|
while(~PINB & gigatronPulseBit); // Then wait for hSync to rise
|
|
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// For polling the game controller
|
|
void sendPulse(byte pin)
|
|
{
|
|
digitalWrite(pin, HIGH);
|
|
delayMicroseconds(50);
|
|
digitalWrite(pin, LOW);
|
|
delayMicroseconds(50);
|
|
}
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
| EEPROM functions |
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// Send a saved file as keystrokes to the Gigatron
|
|
void sendSavedFile()
|
|
{
|
|
#if hasSerial
|
|
Serial.println(F(":Sending from EEPROM"));
|
|
#endif
|
|
word i = fileStart, j = 0; // i is the file index. j is the line index
|
|
int lineDelay = 50; // Default extra delay time for "line feed"
|
|
do {
|
|
byte nextByte = EEPROM.read(i++); // Fetch next byte from saved program
|
|
if(j++ == 0 && nextByte == 255) // EOF. Note that in MSBASIC, 255 means Pi.
|
|
break; // So we only check this after a newline.
|
|
sendController(nextByte, 2); // A single frame is sometimes too fast
|
|
if(nextByte == '\r') // "A carriage return takes more time"
|
|
lineDelay = 300 + j * 50; // Reality: Micro-Soft BASIC is s-l-o-w
|
|
delay((j % 26) ? 20 // Allow Gigatron software to draw the char
|
|
: 300); // And give more time at line wrap
|
|
if(nextByte == '\n')
|
|
{ // End of line
|
|
delay(lineDelay); // Allow some extra time for line processing
|
|
j = 0; // Start of new line
|
|
}
|
|
} while(i < EEPROM.length()); // There may be no space for an EOF symbol
|
|
}
|