gigatron/rom/Contrib/at67/hw/SDCard_ROM/BabelFish/BabelFish.ino
2025-01-28 19:17:01 +03:00

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
}