// 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 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 #include // 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[] Transfer object file from PROGMEM slot ")); 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 Transfer from SD")); Serial.println(F(": J List SD")); Serial.println(F(": . 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 , , // Entry is maximum 15 bytes, <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= 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 }