712 lines
21 KiB
C++
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;
|
|
}
|