Pluggy Reloaded

Using, learning, programming and modding the Gigatron and anything related.
Forum rules
Be nice. No drama.
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

Thanks for the offer! This project is also an excuse for me to learn EDA Tools. I'll see how far I can get with KiCad by myself and might get back to you.
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

After some more prototyping with a Pro Micro Board and messing around with the BabelFish code, I have now managed to get the game controller and the PS/2 keyboard to work simultaneously. I had to temporarily disable serial output from the Gigatron but it shouldn't be too hard to get that working again.

Image

This is the schematic from the photo above:

Image

I have also ordered some Micro SD breakout boards, to test out monsonites suggestion.

After testing those, I'll start laying out a PCB. For now it'll just be a carrier board for the Pro Micro and the Micro SD breakout.


While experimenting I have noticed, that for some reason the Gigatron can be partially powered through the data pin on the game controller port.

With the schematic above, if I power the Pro Micro through USB but disconnect power to the Gigatron, the Blinkenlights will stay on (the animation stops). As soon as I disconnect the data pin the LED's go out.

Since this is less than ideal, do you perhaps have an idea what I could do about this?

I can only think of pulling the data pin low in Software when there's no power from the Gigatron.
User avatar
marcelk
Posts: 488
Joined: 13 May 2018, 08:26

Re: Pluggy Reloaded

Post by marcelk »

I have now managed to get the game controller and the PS/2 keyboard to work simultaneously.
Can you explain or show how you did it? I like to fix it in BabelFish soon, as I'm about to do some experiments with the non-clone Arduino Nano that just arrived. I also plan to fix an old issue, and BabelFish needs an update for MS BASIC as well. I can try to address and test this in the same effort.

P.S.: Your image links aren't showing in my browser (Safari).
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

Ill post my code later today, when I'm back home.
The images from the last post should be attached.
Attachments
screenshot_2019-10-26-173654.png
screenshot_2019-10-26-173654.png (169.53 KiB) Viewed 7650 times
IMG_20191026_153514.jpg
IMG_20191026_153514.jpg (256.3 KiB) Viewed 7650 times
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

What I did to get the Game Controller passthrough working again is basically just manually pulsing the game controllers latch and clock pin while reading from the data pin, and then sending the controller state to the Gigatron every Frame (except when a key of the keyboard is pressed).

I had to reduce the delay for the PS/2 interrupts, because missing a frame when sending the controller state would lead to repeated presses of a button.

To get back the Gigatron-> PC serial interface you would have to add the HSync counting functionality from waitVSync to sendFirstByte (both have to happen simultaneously right after VSync). Ill probably do this when I have the SD Card functionality fully working.

Here's the modified Babelfish.ino for the Game Controller passthrough

Code: Select all


// 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
//
// Select one of the supported platforms in the Tools->Board menu.
//
// Supported:
//      - Arduino/Genuino Uno
//      - Arduino Nano
//      - Arduino/Genuino Micro
//      - ATtiny85 (8 MHz)
//
// (not every microcontroller platform supports all functions)

/*----------------------------------------------------------------------+
 |                                                                      |
 |      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

#if defined(ARDUINO_AVR_NANO)
 // at67's setup
 #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 // PB2

 // 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
#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 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
#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 // XXX Maybe use ~RESET for this some day

 // Pins for PS/2 keyboard
 #define keyboardClockPin PB4
 #define keyboardDataPin  PB3

 // Link to PC/laptop
 #define hasSerial 0
#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 28672 // TODO: find out exact value

 // 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
#endif

/*----------------------------------------------------------------------+
 |                                                                      |
 |      Built-in GT1 images                                             |
 |                                                                      |
 +----------------------------------------------------------------------*/

const byte TinyBASIC_gt1[] PROGMEM = {
  #include "TinyBASIC_v3.h"
};
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 byte bricks_gt1[]    PROGMEM = {
  #include "bricks.h"
};
const byte lines_gt1[]     PROGMEM = {
  #include "Lines.h"
};
const byte life3_gt1[]     PROGMEM = {
  #include "life3.h"
};
const byte starfield_gt1[] PROGMEM = {
  #include "starfield.h"
};
const byte tetris_gt1[]    PROGMEM = {
  #include "tetris.h"
};

const struct { const byte *gt1; const char *name; } gt1Files[] = {
  { TinyBASIC_gt1, "BASIC"                    }, // 3073 bytes
#if maxStorage >= 10000
  { WozMon_gt1,    "WozMon"                   }, // 595 bytes
  { Terminal_gt1,  "Terminal"                 }, // 256 bytes
  { Blinky_gt1,    "Blinky"                   }, // 17 bytes
  { lines_gt1,     "Lines demo [at67]"        }, // 304 bytes
  { life3_gt1,     "Game of Life demo [at67]" }, // 441 bytes
  { starfield_gt1, "Starfield demo [at67]"    }, // 817 bytes
#endif
#if maxStorage >= 30000
  { bricks_gt1,    "Bricks game [xbx]"        }, // 1607 bytes
  { tetris_gt1,    "Tetris game [at67]"       }, // 9868 bytes
#endif
  { NULL,          "-SAVED-"                  },
};

/*----------------------------------------------------------------------+
 |                                                                      |
 |      End config section                                              |
 |                                                                      |
 +----------------------------------------------------------------------*/

/*
 *  Bit masks for pins
 */
byte gigatronDataBit;
byte gigatronLatchBit;
byte gigatronPulseBit;

/*
 *  Loader protocol
 */
#define N 60         // Payload bytes per transmission frame
byte checksum;       // Global is simplest
byte outBuffer[256]; // sendFrame() will read up to index 299 but that's ok.
                     // outBuffer[] is global, because having it on the stack
                     // can cause trouble on the ATtiny85 (not fully clear why)
/*
 *  Game controller button mapping
 */
#define buttonRight  1
#define buttonLeft   2
#define buttonDown   4
#define buttonUp     8
#define buttonStart  16
#define buttonSelect 32
#define buttonB      64
#define buttonA      128
// Note: The kit's game controller gives inverted signals.

/*
 *  Font data
 */
const int tinyfont[96] PROGMEM = {
  #include "tinyfont.h"
};

/*
 *  Terminal mode for upstream host
 */
static bool echo = false;

/*
 *  Non-volatile memory
 */
#include <EEPROM.h>

struct EEPROMlayout {
  byte keymapIndex;
  byte savedFile[];
};

#define fileStart offsetof(struct EEPROMlayout, savedFile)
static word saveIndex = fileStart; // Write pointer into EEPROM for file (BASIC)
static word EEPROM_length;

#define arrayLen(a) ((int) (sizeof(a) / sizeof((a)[0])))
extern const byte nrKeymaps; // From in PS2.ino

/*
 *  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);
    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()
{
  /*
  // Check Gigatron's vPulse for incoming data
  static byte inByte, inBit, inLen;

  critical();
  byte newValue = waitVSync();
  nonCritical();

  switch (newValue) {

    case 9:                            // Received one bit
      inByte |= inBit;
      // !!! FALL THROUGH !!!

    case 7:                            // Received zero bit
      inBit <<= 1;

      if (saveIndex >= EEPROM_length)  // EEPROM overflow
        if (inLen > 0)                 // Only break if this can't be a new program
          sendController(3, 10);       // Send long Ctrl-C back

      if (inBit != 0)
        break;

      EEPROM.write(saveIndex++, inByte);// Store every full byte

      #if hasSerial
        if (inByte == 10)
          Serial.print('\r');
        Serial.print((char)inByte);    // Forward to host
        noInterrupts();
      #endif

      if (inByte == 10) {              // End of line?
        EEPROM.write(saveIndex, 255);  // Terminate, in case this was the last line
        if (inLen == 0)                // An empty line clears the old program
          saveIndex = fileStart;
        inLen = 0;
      } else
        inLen++;

      inByte = 0;                      // Prepare for next byte
      inBit = 1;
      break;

    default:
      inByte = 0;                      // Reset incoming data state
      inBit = 1;
      inLen = 0;
      break;
  }*/
  // Game controller pass through
  #if gameControllerDataPin >= 0
    critical();
    byte gameControllerData = 0;
    digitalWrite(gameControllerLatchPin, HIGH);
    delayMicroseconds(50);
    digitalWrite(gameControllerLatchPin, LOW);
    delayMicroseconds(50);
    
    for(byte i = 0; i < 8; i++) {
      gameControllerData <<= 1;
      gameControllerData |= digitalRead(gameControllerDataPin);

      digitalWrite(gameControllerPulsePin, HIGH);
      delayMicroseconds(50);
      digitalWrite(gameControllerPulsePin, LOW);
      delayMicroseconds(50);
    }
    sendFirstByte(gameControllerData);
    nonCritical();
  #endif

  // PS/2 keyboard events
  delay(10);                           // Allow PS/2 interrupts for a reasonable window
  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)doTransfer(gt1Files[f-2].gt1); // Send built-in
        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
    static char line[32], next = 0, last;
    static byte lineIndex = 0;
    if (Serial.available()) {
      last = next;
      char next = Serial.read();
      sendEcho(next, last);
      if (lineIndex < sizeof line)
        line[lineIndex++] = next;
      if (next == '\r' || next == '\n') {
        line[lineIndex-1] = '\0';
        doCommand(line);
        lineIndex = 0;
      }
    }
  #endif
}

void prompt()
{
  #if hasSerial
    Serial.println(detectGigatron() ? ":Gigatron OK" : "!Gigatron offline");
    Serial.println("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.20 && 0.90 <= hSync && hSync <= 1.10;
}

void sendEcho(char next, char last)
{
  #if hasSerial
    if (echo)
      switch (next) {
        case 127:  Serial.print("\b \b"); break;
        case '\n': if (last == '\r')      break; // else FALL THROUGH
        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': if (0 <= arg && arg < arrayLen(gt1Files))
              doTransfer(gt1Files[arg].gt1);  break;
  case 'U': doTransfer(NULL);                 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("!Unknown command (type 'H' for help)");
    #endif
    ;
  }
  prompt();
}

void doVersion()
{
  #if hasSerial
    Serial.println(":BabelFish platform=" platform);
    Serial.println(":Pins:");
    #define V(s) #s
    #define Q(s) V(s)
    Serial.println(": Gigatron data=" Q(gigatronDataPin) " latch=" Q(gigatronLatchPin) " pulse=" Q(gigatronPulsePin));
    Serial.println(": Keyboard clock=" Q(keyboardClockPin) " data=" Q(keyboardDataPin));
    Serial.println(": Controller data=" Q(gameControllerDataPin));
    Serial.println(":EEPROM:");
    Serial.print(": size=");
    Serial.print(EEPROM.length());
    Serial.print(" mapping=");
    Serial.println(getKeymapName());
    Serial.println(":PROGMEM slots:");
    for (byte i=0; i<arrayLen(gt1Files); i++) {
      Serial.print(": P");
      Serial.print(i);
      Serial.print(") ");
      Serial.println(gt1Files[i].name);
    }
    doEcho(echo);
    Serial.println(":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(":Echo ");
    Serial.println(value ? "on" : "off");
  #endif
}

void doHelp()
{
  #if hasSerial
    Serial.println(":Commands are");
    Serial.println(": V        Show configuration");
    Serial.println(": H        Show this help");
    Serial.println(": R        Reset Gigatron");
    Serial.println(": L        Start Loader");
    Serial.println(": M        Show key mapping or menu in Loader screen");
    Serial.println(": P[<n>]   Transfer object file from PROGMEM slot <n> [1..12]");
    Serial.println(": U        Transfer object file from USB");
    Serial.println(": .<text>  Send text line as ASCII key strokes");
    Serial.println(": C        Toggle echo mode (default off)");
    Serial.println(": T        Enter terminal mode");
    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(int n)
{
  // Soft reset: hold start for >128 frames (>2.1 seconds)
  #if hasSerial
    Serial.println(":Resetting Gigatron");
    Serial.flush();
  #endif
  sendController(~buttonStart, n ? n : 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 (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], 1);
    delay(20); // Allow Gigatron software to process key code
  }
  // And terminal with a CR
  sendController('\n', 1);
  delay(50); // Allow Gigatron software to process line
}

// 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(":Entering terminal mode");
    Serial.println(":Exit with Ctrl-D");
    char next = 0, last;
    byte out;
    bool ansi = false;
    for (;;) {
      if (Serial.available()) {
        last = next;
        next = Serial.read();
        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;
          case '\n': if (last == '\r')             continue; // Swallow
          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;
      }
    }
  #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;
}

// 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
    if (!waitVSync()) {
      Serial.print("!Failed");
      return;
    }
  #endif

  #if hasSerial
    #define readNext()\
      if (gt1)\
        nextByte = pgm_read_byte(gt1++);\
      else {\
        nextByte = nextSerial();\
        if (nextByte < 0) return;\
      }
    #define ask(n)\
      if (!gt1) {\
        Serial.print(n);\
        Serial.println("?");\
      }
  #else
    #define readNext() (nextByte = pgm_read_byte(gt1++))
    #define ask(n)
  #endif

  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();
      outBuffer[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);

    // 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, 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("!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
void 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++)
    sendFirstByte(value);
  nonCritical();

  PORTB |= gigatronDataBit; // Send 1 when idle
}

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
}

void 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
  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 |= gigatronDataBit;
    else
      PORTB &= ~gigatronDataBit;

    // Wait for bit transfer at horizontal sync POSITIVE edge.
    while (PINB & gigatronPulseBit)  // Ensure hSync is LOW first
      ;
    while (~PINB & gigatronPulseBit) // Then wait for hSync to rise
      ;
  }
  checksum += value;
}

// 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;
}

/*----------------------------------------------------------------------+
 |                                                                      |
 |      EEPROM functions                                                |
 |                                                                      |
 +----------------------------------------------------------------------*/

// Send a saved file as keystrokes to the Gigatron
void sendSavedFile()
{
    word i = fileStart;
    do {
      byte nextByte = EEPROM.read(i);
      if (nextByte == 255)
        break;
      sendController(nextByte, 1);
      delay(nextByte == 10 ? 70 : 20);  // Allow Gigatron software to process byte
    } while (++i < EEPROM.length());
}

Ill post the SD Card code when its fully working.
User avatar
marcelk
Posts: 488
Joined: 13 May 2018, 08:26

Re: Pluggy Reloaded

Post by marcelk »

Thanks. When you press a game controller button now, doesn't it get sent intermittently to the Gigatron? It appears the code will alternate between calling waitVSync() and sendFirstByte() in that case. Both wait for the vertical pulse. For example, does the soft reset function work with this change?

I was thinking of looping locally, like this:

Code: Select all

    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)
        break;
      critical();                      // Forward byte to Gigatron
      sendFirstByte(serialByte);
      nonCritical();
    }
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

The code for the Gigatron->PC Serial link containing the call to waitVSync() is commented out, so game controller data ist sent every VSync Pulse. Should have probably deleted that part to make it more clear. As I mentioned in my last post this serial functionality is something I want to bring back by moving the HSync counting to sendFirstByte() but havent gotten around to that yet. Soft reset works with the code I posted.
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

After some consideration your solution seems to be the better option, since its more consistent with the PS/2 code and doesn't require reworking the serial code. Only downside is that while a button is held down nothing else works. But that was already the case with the PS/2 keyboard and shouldn't affect the regular use case.
User avatar
marcelk
Posts: 488
Joined: 13 May 2018, 08:26

Re: Pluggy Reloaded

Post by marcelk »

I have this part (pass through) working now on my Arduino Nano as well, so I just pushed it into GitHub.

Edit: I found that my

Code: Select all

       critical();                     // Forward byte to Gigatron
       sendFirstByte(serialByte);
       nonCritical();
is better done as

Code: Select all

      sendController(serialByte, 1);   // Forward byte to Gigatron
because that also keeps the data line high while idle and that prevents some trouble.
norgate
Posts: 50
Joined: 20 Oct 2019, 16:50

Re: Pluggy Reloaded

Post by norgate »

Very nice!

In the meanwhile I have managed to get the SD card file transfer finally working. You can check out the code in my fork of the git repo:

https://github.com/DerTyp321/gigatron-rom/

Turns out the issue I was having with that was a stack overflow, due to the Atmega32U4 only having 2K of SRAM. My "fix" was to shorten all the menu strings. Anybody have a better idea? Maybe putting the strings into progmem somehow?
Post Reply