// Concept tester for interfacing with the // Gigatron TTL microcomputer using a microcontroller // hooked to the input port (J4). // 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 (sendGt1.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. Controlling the Gigatron over USB from a PC/laptop // Not every microcontroller supports all functions. // Select 1 of the platforms: #define ArduinoUno 0 // Default #define ArduinoNano 1 #define ArduinoMicro 0 #define ATtiny85 0 // Select a built-in GT1 image: const byte gt1File[] PROGMEM = { //#include "Blinky.h" // Blink pixel in middle of screen #include "Lines.h" // Draw randomized lines (at67) }; // The object file is embedded (in PROGMEM) in GT1 format. It would be // GREAT if we can find a way to receive the file over the Arduino's // serial interface without adding upstream complexity. But as the // Arduino's 2K of RAM can't buffer an entire file at once, some // intelligence is needed there and we haven't found a good way yet. // The Arduino doesn't implement any form of flow control on its // USB/serial interface (RTS/CTS or XON/XOFF). // This interface program can also receive data over the USB serial interface. // Use the sendGt1.py Python program on the computer to send a file. // The file must be in GT1 format (.gt1 extension) // For example: // python sendGt1.py life3.gt1 // Todo/idea list: // XXX Wild idea: let the ROM communicate back by modulating vPulse // XXX Hardware: Put reset line on the DB9 jack // XXX Hardware: Put an output line on the DB9 jack // XXX Keyboard: Map Ctrl-Alt-Del to Gigatron reset (instead of PageUp) // XXX Keyboard: Map Enter to both newline AND buttonA // XXX Keyboard: Delete = buttonA (same code 0x7f). Change delete code? // XXX Keyboard: Is it possible to mimic key hold-down properly??? // XXX Timeouts/reset in case of hanging (dead man switch function?) // XXX Embed a Gigatron Terminal program. Or better: GigaMon /*----------------------------------------------------------------------+ | 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 ArduinoUno #define version "ArduinoUno" // Pins for Gigatron #define SER_DATA PB5 #define SER_LATCH PB4 #define SER_PULSE PB3 // Pins for PS/2 keyboard (Arduino Uno) #define keyboardClockPin PB3 // Pin 2 or 3 for IRQ #define keyboardDataPin PB4 // Any available free pin // Link to PC/laptop #define hasSerial 1 // Controller pass through #define hasController 0 #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 #if ArduinoNano #define version "ArduinoNano" // Pins for Gigatron #define SER_DATA PB5 #define SER_LATCH PB4 #define SER_PULSE PB3 // Pins for Controller #define JOY_DATA PB2 // Pins for PS/2 keyboard (Arduino Nano) #define keyboardClockPin PB3 // Pin 2 or 3 for IRQ #define keyboardDataPin PB4 // Any available free pin // Link to PC/laptop #define hasSerial 1 // Controller pass through #define hasController 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 // --------------------+ // Reset | // Arduino +---+ | // Micro | O | | // +---+ | // | // 2 4 6 | // ICSP-> o o o | // Port .o o o | // 1 3 5 | // --------------------+ #if ArduinoMicro #define version "ArduinoMicro" // Pins for Gigatron #define SER_DATA PB1 #define SER_LATCH PB3 #define SER_PULSE PB2 // Pins for PS/2 keyboard (XXX These are still for Arduino Uno) #define keyboardClockPin PB3 // Pin 2 or 3 for IRQ #define keyboardDataPin PB4 // Any available free pin // Link to PC/laptop #define hasSerial 1 // Controller pass through #define hasController 0 #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 ATtiny85 #define version "ATtiny85" // Pins for Gigatron #define SER_DATA PB2 #define SER_LATCH PB1 #define SER_PULSE PB0 // Pins for PS/2 keyboard #define keyboardClockPin PB4 #define keyboardDataPin PB3 // Link to PC/laptop #define hasSerial 0 // Controller pass through #define hasController 0 // PS2Keyboard.h uses attachInterrupt() which doesn't work on the ATtiny85. // Workaround as follows: void ps2interrupt(void); // As provided by PS2Keyboard ISR(PCINT0_vect) { if (~PINB & (1 << keyboardClockPin)) // FALLING edge of PS/2 clock ps2interrupt(); } #endif /*----------------------------------------------------------------------+ | End config section | +----------------------------------------------------------------------*/ /* Loader protocol */ #define N 60 // Payload bytes per transmission frame byte checksum; // Global is simplest /* PS/2 keyboard hookup to Arduino */ #include // Install from the Arduino IDE's Library Manager PS2Keyboard keyboard; const byte terminalGt1[] PROGMEM = { #include "WozMon.h" // Monitor program ported from Apple-1 }; /* 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 controller gives inverted signals. /* Emulator control */ #define EMU_PS2_LEFT 1 #define EMU_PS2_RIGHT 2 #define EMU_PS2_UP 3 #define EMU_PS2_DOWN 4 #define EMU_PS2_START 7 #define EMU_PS2_SELECT 8 #define EMU_PS2_INPUT_A 9 #define EMU_PS2_INPUT_B 27 #define EMU_PS2_CR 13 #define EMU_PS2_DEL 127 #define EMU_PS2_ENABLE 5 #define EMU_PS2_DISABLE 6 bool emulatorControl = false; /* Setup runs once when the Arduino wakes up */ void setup() { // Enable output pin (pins are set to input by default) PORTB |= 1 << SER_DATA; // Send 1 when idle DDRB = 1 << SER_DATA; // Open upstream communication #if hasSerial Serial.begin(115200); #endif doVersion(); // 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); // PS/2 keyboard should be awake by now #if !ATtiny85 keyboard.begin(keyboardDataPin, keyboardClockPin); #else keyboard.begin(keyboardDataPin, 255); GIMSK |= 1 << PCIE; // Pin change interrupt enable PCMSK |= 1 << keyboardClockPin; // Pin change mask #endif prompt(); } /* Loop runs repeatedly */ void loop() { // Controller pass through #if hasController ((PINB >> JOY_DATA) & 1) ? PORTB |= 1 << SER_DATA : PORTB &= ~(1 << SER_DATA); #endif #if hasSerial static char line[20]; static byte lineIndex = 0; if (Serial.available()) { byte next = Serial.read(); if (lineIndex < sizeof line) line[lineIndex++] = next; if (next == '\n') { line[lineIndex - 1] = '\0'; (emulatorControl) ? doEmulator(line) : doCommand(line); lineIndex = 0; } } #endif if (keyboard.available()) { char c = keyboard.read(); switch (c) { // XXX These mappings are for testing purposes only case PS2_PAGEDOWN: sendController(~buttonSelect, 1); break; case PS2_PAGEUP: sendController(~buttonStart, 128 + 32); break; // XXX Change to Ctrl-Alt-Del case PS2_TAB: sendController((byte)~buttonA, 1); break; #if !ATtiny85 case PS2_ESC: sendController(~buttonB, 1); break; #else case PS2_ESC: doTransfer(terminalGt1); break; // XXX HACK Find some proper short-cut. Ctrl-T? #endif case PS2_LEFTARROW: sendController(~buttonLeft, 2); break; case PS2_RIGHTARROW: sendController(~buttonRight, 2); break; case PS2_UPARROW: sendController(~buttonUp, 2); break; case PS2_DOWNARROW: sendController(~buttonDown, 2); break; case PS2_ENTER: sendController('\n', 1); break; case PS2_DELETE: sendController(127, 1); break; default: sendController(c, 1); break; } delay(50); // Allow Gigatron software to process key code } } void prompt() { #if hasSerial Serial.println(detectGigatron() ? ":Gigatron OK" : "!Gigatron offline"); Serial.println("\nCmd?"); #endif } bool detectGigatron() { unsigned long timeout = millis() + 85; long T[2][2] = {{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 >> SER_LATCH) & 1 ][ (pinb >> SER_PULSE) & 1 ]++; } float S = T[0][0] + T[0][1] + T[1][0] + T[1][1] + .1; // Avoid zero division (pedantic) float vSync = (T[0][0] + T[0][1]) / ( 8 * S / 521); // Adjusted vSync signal float hSync = (T[0][0] + T[1][0]) / (96 * S / 800); // Standard hSync signal // Check that vSync and hSync characteristics look normal return 0.95 <= vSync && vSync <= 1.20 && 0.95 <= hSync && hSync <= 1.05; } void doCommand(char line[]) { switch (toupper(line[0])) { case 'V': doVersion(); break; case 'H': doHelp(); break; case 'R': doReset(); break; case 'L': doLoader(); break; case 'P': doTransfer(gt1File); break; case 'U': doTransfer(NULL); 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((byte)~buttonA, 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; case EMU_PS2_ENABLE: emulatorControl = true; break; #if hasSerial default: Serial.println("!Unknown command (type 'H' for help)"); #endif } prompt(); } void doEmulator(char line[]) { switch (line[0]) { case EMU_PS2_LEFT: sendController(~buttonLeft, 2); break; case EMU_PS2_RIGHT: sendController(~buttonRight, 2); break; case EMU_PS2_UP: sendController(~buttonUp, 2); break; case EMU_PS2_DOWN: sendController(~buttonDown, 2); break; case EMU_PS2_START: sendController(~buttonStart, 128 + 32); break; case EMU_PS2_SELECT: sendController(~buttonSelect, 2); break; case EMU_PS2_INPUT_A: sendController((byte)~buttonA, 2); break; case EMU_PS2_INPUT_B: sendController(~buttonB, 2); break; case EMU_PS2_CR: sendController('\n', 2); break; case EMU_PS2_DEL: sendController(127, 2); break; case EMU_PS2_DISABLE: emulatorControl = false; break; default: sendController(line[0], 2); break; } prompt(); } void doVersion() { #if hasSerial Serial.println(":Gigatron Interface Adapter [" version "]\n" ":Type 'H' for help"); #endif } void doHelp() { #if hasSerial Serial.println(":Commands are"); Serial.println(": V Show version"); Serial.println(": H Show this help"); Serial.println(": R Reset Gigatron"); Serial.println(": L Start Loader"); Serial.println(": P Transfer object file from PROGMEM"); Serial.println(": U Transfer object file from USB"); Serial.println(": W/A/S/D Up/left/down/right arrow"); Serial.println(": Z/X A/B button "); Serial.println(": Q/E Select/start button"); #endif } void doReset() { // Soft reset: hold start for >128 frames (>2.1 seconds) #if hasSerial Serial.println(":Resetting Gigatron"); Serial.flush(); #endif sendController(~buttonStart, 128 + 32); // Wait for main menu to be ready delay(1500); } void doLoader() { // Navigate menu. 'Loader' is at the bottom #if hasSerial Serial.println(":Starting Loader from menu"); Serial.flush(); #endif for (int i = 0; i < 10; i++) { sendController(~buttonDown, 2); delay(50); } // Start 'Loader' application on Gigatron sendController((byte)~buttonA, 2); // Wait for Loader to be running delay(1000); } // 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(const byte *gt1) { int nextByte; #if hasSerial #define readNext() {\ nextByte = gt1 ? pgm_read_byte(gt1++) : nextSerial();\ if (nextByte < 0) return;\ } #define ask(n)\ if (!gt1) {\ Serial.print(n);\ Serial.println("?");\ } #else #define readNext() {\ nextByte = pgm_read_byte(gt1++);\ if (nextByte < 0) return;\ } #define ask(n) #endif byte segment[300] = {0}; // Multiple of N for padding ask(3); readNext(); word address = nextByte; // Any number n of segments (n>0) do { // Segment start and length readNext(); address = (address << 8) + nextByte; readNext(); int len = nextByte ? nextByte : 256; ask(len); // Copy data into send buffer for (int i = 0; i < len; i++) { readNext(); segment[i] = nextByte; } // Check that segment doesn't cross the page boundary if ((address & 255) + len > 256) { #if hasSerial Serial.println("!Data error (page overflow)"); #endif return; } // Send downstream #if hasSerial Serial.print(":Loading "); Serial.print(len); Serial.print(" bytes at $"); Serial.println(address, HEX); Serial.flush(); #endif sendGt1Segment(address, len, segment); // Signal that we're ready to receive more ask(3); readNext(); address = nextByte; } while (address != 0); // Two bytes for start address readNext(); address = nextByte; readNext(); address = (address << 8) + nextByte; if (address != 0) { #if hasSerial Serial.print(":Executing from $"); Serial.println(address, HEX); Serial.flush(); #endif sendGt1Execute(address, segment + 240); } } int nextSerial() { #if hasSerial unsigned long timeout = millis() + 5000; while (!Serial.available() && millis() < timeout) ; int nextByte = Serial.read(); if (nextByte < 0) Serial.println("!Timeout error (no data)"); return nextByte; #endif } // 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 data[]) { noInterrupts(); byte n = min(N, len); resetChecksum(); // Send segment data for (int i = 0; i < len; i += n) { n = min(N, len - i); sendFrame('L', n, address + i, data + i); } interrupts(); // Wait for vBlank 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 & (1 << SER_LATCH)) // ~160 us ; } // Send execute command void sendGt1Execute(word address, byte data[]) { noInterrupts(); resetChecksum(); sendFrame('L', 0, address, data); interrupts(); } // Pretend to be a game controller // Send the same byte a few frames like a human user void sendController(byte value, int n) { noInterrupts(); // Send controller code for n frames // E.g. 4 frames = 3/60s = ~50 ms for (int i = 0; i < n; i++) { sendFirstByte(value); PORTB |= 1 << SER_DATA; // Send 1 when idle } interrupts(); // So delay() can work again } 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 |= 1 << SER_DATA; // Send 1 when idle } void sendFirstByte(byte value) { // Wait vertical sync NEGATIVE edge to sync with loader while (~PINB & (1 << SER_LATCH)) // Ensure vSync is HIGH first ; while (PINB & (1 << SER_LATCH)) // Then wait for vSync to drop ; // Send first bit if (value & 128) PORTB |= 1 << SER_DATA; else PORTB &= ~(1 << SER_DATA); // Wait for bit transfer at horizontal sync POSITIVE edge. // This timing is tight for the first bit of the first byte and // the reason that interrupts must be disabled on the microcontroller. while (PINB & (1 << SER_PULSE)) // Ensure hSync is LOW first ; while (~PINB & (1 << SER_PULSE)) // Then wait for hSync to rise ; // Send remaining bits sendBits(value, 7); } // Send n bits, highest first void sendBits(byte value, byte n) { for (byte bit = 1 << (n - 1); bit; bit >>= 1) { // Send next bit if (value & bit) PORTB |= 1 << SER_DATA; else PORTB &= ~(1 << SER_DATA); // Wait for bit transfer at horizontal sync POSITIVE edge. while (PINB & (1 << SER_PULSE)) // Ensure hSync is LOW first ; while (~PINB & (1 << SER_PULSE)) // Then wait for hSync to rise ; } checksum += value; }