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

712 lines
21 KiB
C++

// 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 <PS2Keyboard.h> // 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;
}