Sunday, December 14, 2014

Arduino: ColecoVision Cartridge Reader

With the numerous emulators and documentation on classic gaming consoles available today, it is easier than ever to create a “homebrew” video game for one of these classic game consoles (see http://en.wikipedia.org/wiki/Homebrew_(video_games) for more information about homebrew video games). Even though it is easier than it used to be, it can still be quite a challenge. It often helps to see how existing games work before creating a new one from scratch.

This article explains how to create a ColecoVision cartridge reader using an Arduino UNO (or similar product) and a PC.
 

What You Need

Note: If using an Arduino board with 29 or more digital I/O pins (e.g. the Arduino Due, Arduino MEGA ADK, Arduino Mega 2560, etc.), this can be built without using the two 8-bit shift registers (i.e. the 74HC595 or STP16C596).

ColecoVision Cartridge Overview

Fortune Builder Cartridge

ColecoVision cartridges typically come in sizes of 8K, 16K, 24K, or 32K. I have also encountered a few 12K cartridges. They generally have one to four 8K rom chips, although I have seen a few exceptions. For example, the Pitfall cartridge I own has a single 16K rom chip.


Inside Cartridge with Case
Image of the inside of the 24k Donkey Kong cartridge.












ColecoVision Cartridge Pin Outs

ColecoVision cartridges have 15 address pins, 8 data pins, 4 low-enabled chip select pins, 2 ground pins, and a Vcc pin. The layout of the 30 ColecoVision pins is shown below:

Cartridge Slot With Labels

Not Enough Pins

The Arduino UNO only has 20 pins that can be used for digital I/O. Two of these are used for the serial communication to the host PC, and I avoid using pin 13, since it is wired to the LED on the Arduino board. That leaves only 17 pins on the Arduino to connect to the 27 pins (15 address, 8 data, and 4 chip select) on the ColecoVision cartridge.






Shift Register Circuit

Shift Register Circuit Diagram
In order to reduce the number of Arduino pins required, a shift register circuit can be used that allows the Arduino to set the cartridge’s 15 address pins using only three Arduino pins. This circuit is described in detail on the Arduino website at http://arduino.cc/en/tutorial/ShiftOut. The LEDs and resistors shown in the example are not required for the cartridge reader, but I included them so I could see what the cartridge reader was doing.









 
 
 
Connections Table

From Pin To Pin
Cartridge Connector (A01) 9 74HC595 #1 1
Cartridge Connector (A02) 11 74HC595 #1 2
Cartridge Connector (A03) 15 74HC595 #1 3
Cartridge Connector (A04) 17 74HC595 #1 4
Cartridge Connector (A05) 21 74HC595 #1 5
Cartridge Connector (A06) 23 74HC595 #1 6
Cartridge Connector (A07) 25 74HC595 #1 7
Arduino GND 74HC595 #1 8
Arduino Vcc 74HC595 #1 10
Arduino 11 74HC595 #1 11
Arduino 12 74HC595 #1 12
Arduino GND 74HC595 #1 13
Arduino 10 74HC595 #1 14
Cartridge Connector (A00) 7 74HC595 #1 15
Arduino Vcc 74HC595 #1 16
Cartridge Connector (A09) 26 74HC595 #2 1
Cartridge Connector (A10) 16 74HC595 #2 2
Cartridge Connector (A11) 14 74HC595 #2 3
Cartridge Connector (A12) 24 74HC595 #2 4
Cartridge Connector (A13) 19 74HC595 #2 5
Cartridge Connector (A14) 20 74HC595 #2 6
Arduino GND 74HC595 #2 8
Arduino Vcc 74HC595 #2 10
Arduino 11 74HC595 #2 11
Arduino 12 74HC595 #2 12
Arduino GND 74HC595 #2 13
74HC595 #1 9 74HC595 #2 14
Cartridge Connector (A08) 28 74HC595 #2 15
Arduino Vcc 74HC595 #2 16

Chip Select Pins

The four chip select pins on the cartridge correspond to the four ROM chips that may be present in the cartridge. The first chip select pin (Chip Select 0x8000) should be toggled (high to low) when reading any addresses between 0x8000 and 0x9FFF. Likewise, Chip Select 0xA000 should be toggled for addresses between 0xA000 and 0xBFFF, Chip Select 0xC000 for 0xC000 through 0xDFFF, and Chip Select 0xE000 for 0xE000 through 0xFFFF.

From Pin To Pin
Arduino A0 Cartridge Connector (Chip Select 0x8000) 18
Arduino A1 Cartridge Connector (Chip Select 0xA000) 22
Arduino A2 Cartridge Connector (Chip Select 0xC000) 2
Arduino A3 Cartridge Connector (Chip Select 0xE000) 27

Data, Vcc, and Ground Pins

Once the address pins and chip select pins have been wired up, the data, Vcc, and ground pins of the cartridge can be connected to the Arduino.

From Pin To Pin
Arduino Vcc Cartridge Connector (Vcc) 30
Arduino GND Cartridge Connector (GND) 13, 29
Arduino 2 Cartridge Connector (Data 0) 5
Arduino 3 Cartridge Connector (Data 1) 3
Arduino 4 Cartridge Connector (Data 2) 1
Arduino 5 Cartridge Connector (Data 3) 4
Arduino 6 Cartridge Connector (Data 4) 6
Arduino 7 Cartridge Connector (Data 5) 8
Arduino 8 Cartridge Connector (Data 6) 10
Arduino 9 Cartridge Connector (Data 7) 12

Arduino Sketch File

The following Arduino Sketch file causes the Arduino to wait for a line from the host computer. If the line read in is “READ ALL”, the Arduino will do the following:
  • Send a “START:” line to the host computer.
  • Read all of the data from the cartridge and send it to the host computer in HEX, one byte per line.
  • Send a “:END” line to the host computer.
One the cartridge data has been sent to the host computer, the Arduino is ready for its next command from the host computer.


// ColecoVision / ADAM Cartridge Reader
// for the Arduino UNO
// 2014-11-25
//----------------------------------------------------------------------------------

// Arduino Pins
const int gcChipSelectLine[4] = { A0, A1, A2, A3 };
const int gcShiftRegisterClock = 11;
const int gcStorageRegisterClock = 12;
const int gcSerialAddress = 10;
const int gcDataBit[8] = { 2, 3, 4, 5, 6, 7, 8, 9 };

// Shifts a 16-bit value out to a shift register.
// Parameters:
//   dataPin - Arduino Pin connected to the data pin of the shift register.
//   clockPin - Arduino Pin connected to the data clock pin of the shift register.
//----------------------------------------------------------------------------------
void shiftOut16(int dataPin, int clockPin, int bitOrder, int value)
{
  // Shift out highbyte for MSBFIRST
  shiftOut(dataPin, clockPin, bitOrder, (bitOrder == MSBFIRST ? (value >> 8) : value));  
  // shift out lowbyte for MSBFIRST
  shiftOut(dataPin, clockPin, bitOrder, (bitOrder == MSBFIRST ? value : (value >> 8)));
}

// Select which chip on the cartridge to read (LOW = Active).
// Use -1 to set all chip select lines HIGH.
//----------------------------------------------------------------------------------
void SelectChip(byte chipToSelect)
{
  for(int currentChipLine = 0; currentChipLine < 4; currentChipLine++)
  {
    digitalWrite(gcChipSelectLine[currentChipLine], (chipToSelect != currentChipLine));
  }
}

// Set Address Lines
//----------------------------------------------------------------------------------
void SetAddress(unsigned int address)
{
    SelectChip(-1);
  
    // Disable shift register output while loading address
    digitalWrite(gcStorageRegisterClock, LOW);
    
    // Write Out Address
    shiftOut16(gcSerialAddress, gcShiftRegisterClock, MSBFIRST, address);  

    // Enable shift register output
    digitalWrite(gcStorageRegisterClock, HIGH);
    
    int chipToSelect;
    
    if (address < 0xA000) {
      chipToSelect = 0;
    } else if (address < 0xC000) {
      chipToSelect = 1;
    } else if (address < 0xE000) {
      chipToSelect = 2;
    } else {
      chipToSelect = 3;
    }
    SelectChip(chipToSelect);
}

// Read data lines
//----------------------------------------------------------------------------------
void ReadDataLines()
{
  const char cHexLookup[16] = {
    '0', '1', '2', '3', '4', '5', '6', '7', 
    '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    
  int highNibble = 0;
  int lowNibble = 0;
  boolean dataBits[8];
  char byteReadHex[4];

  for(int currentBit = 0; currentBit < 8; currentBit++)
  {
    dataBits[currentBit] = digitalRead(gcDataBit[currentBit]);
  }

  highNibble = (dataBits[7] << 3) + (dataBits[6] << 2) + (dataBits[5] << 1) + dataBits[4];
  lowNibble = (dataBits[3] << 3) + (dataBits[2] << 2) + (dataBits[1] << 1) + dataBits[0];

  Serial.write(cHexLookup[highNibble]);
  Serial.write(cHexLookup[lowNibble]);
  Serial.println();
}

// Read all of the data from the cartridge.
//----------------------------------------------------------------------------------
void ReadCartridge()
{
  unsigned int baseAddress = 0x8000;
  
  Serial.println("START:");
  
  // Read Current Chip (cartridge is 32K, each chip is 8k)
  for (unsigned int currentAddress = 0; currentAddress < 0x8000; currentAddress++) 
  {
    SetAddress(baseAddress + currentAddress);
    ReadDataLines();  
  }
  
  Serial.println(":END");
}

// Returns the next line from the serial port as a String.
//----------------------------------------------------------------------------------
String SerialReadLine()
{
  const int BUFFER_SIZE = 81;
  char lineBuffer[BUFFER_SIZE];
  int currentPosition = 0;
  int currentValue;
  
  do
  {
    // Read until we get the next character
    do
    {
      currentValue = Serial.read();
    } while (currentValue == -1);
    
    // ignore '\r' characters
    if (currentValue != '\r')
    {
      lineBuffer[currentPosition] = currentValue;
      currentPosition++;
    } 
  
  } while ((currentValue != '\n') && (currentPosition < BUFFER_SIZE));
  lineBuffer[currentPosition-1] = 0;
  
  return String(lineBuffer);
}

// Indicate to remote computer Arduino is ready for next command.
//----------------------------------------------------------------------------------
void ReadyForCommand()
{
  Serial.println("READY:");
}

void setup()
{
  // Setup Serial Monitor
  Serial.begin(57600);
  
  // Setup Chip Select Pins
  for(int chipLine = 0; chipLine < 4; chipLine++)
  {
    pinMode(gcChipSelectLine[chipLine], OUTPUT);
  }
  
  // Setup Serial Address Pins
  pinMode(gcShiftRegisterClock, OUTPUT);
  pinMode(gcStorageRegisterClock, OUTPUT);
  pinMode(gcSerialAddress, OUTPUT);
  
  // Setup Data Pins
  for(int currentBit = 0; currentBit < 8; currentBit++)
  {
    pinMode(gcDataBit[currentBit], INPUT_PULLUP);
  }
  
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only.
  }  
  
  // Reset Output Lines
  SetAddress(0);
  
  ReadyForCommand();
}

void loop()
{
  if (Serial.available() > 0)
  {
    String lineRead = SerialReadLine();
    lineRead.toUpperCase();
    
    if (lineRead == "READ ALL")
    {
      ReadCartridge();
    } // lineRead = "Read All"
    
    ReadyForCommand();
    
  } // Serial.available
}


If you are fortunate enough to be using an Arduino board with 29 or more digital I/O pins (e.g. the Arduino Due, Arduino MEGA ADK, Arduino Mega 2560, etc.) and you are not using the shift register circuit, the SetAddress function in the sketch file above will need to be re-written.

PC Software

Reading a Cartridge
I wrote a little Windows application which can read the ColecoVision cartridge data sent by the Arduino, display it on the screen, and save it to a file.

The Windows executable and associated sketch file can be downloaded at ColecoVisionCartridgeReader.zip. The source code for the application and associated sketch file can be downloaded at ColecoVisionCartridgeReaderSource.zip. The latest version of the source code can be found on GitHub at MHeironimus/ColecoVisionCartridgeReader.

After the program reads the contents of the cartridge from the Arduino, it truncates any 8K sections from the end of the cartridge that are blank (i.e. all bytes are set to FF).

Reading a Cartridge

Cartridge Reader with Cartridge
Once all of the circuits have been built and connected to the Arduino, the sketch file has been loaded onto the Arduino, and the ColecoVisionCartridgeReader.exe is installed on the host PC, a ColecoVision cartridge can be read.













Recommended steps to follow:
  1. Insert the cartridge into the connector.
  2. Plug the Arduino into the PC.
  3. Start the ColecoVisionCartridgeReader.exe application.
  4. Select File -> Read From Arduino from the main menu.
  5. Verify the settings are correct and click the Read button.
The progress dialog should appear and within 30 seconds the contents of the cartridge should appear on the screen.

Observations

The first two bytes of all ColecoVision cartridges are 55 and AA. If the AA comes first, the cartridge displays the standard ColecoVision title screen (e.g. standard ColecoVision cartridges like Donkey Kong, Mouse Trap, Zaxxon, etc.). If the 55 comes first, the cartridge skips the standard title screen (e.g. third-party cartridges like Q*Bert, Frogger, Pitfall, etc.).

Interesting Cartridges to Read

Cartridge Reader with Donkey Kong Loaded

Almost everyone who owns a ColecoVision has a Donkey Kong cartridge, since it came with the game console. There are at least two different versions of this cartridge in circulation. The first edition of this cartridge was 24K, but the second edition was only 16K. Using the Arduino ColecoVision Cartridge reader, you can determine which edition you have. Another way to tell is to look at Pauline’s umbrella on the second and third levels of the game. In the 24K version of the cartridge, the umbrella has a glitch shown in the following screenshot, but the updated 16K version of the cartridge does not.

24k Version

Donkey Kong 24k Version

16k Version

Donkey Kong 16k Version

Another interesting cartridge to look at is Fortune Builder. It is one of the few 32K cartridges. It also has a large number of text strings, which can be interesting to read through.

Cartridge Reader with Fortune Builder Loaded

Possible Future Enhancements

  • Detect 12K cartridges and truncate the duplicate 4K at the end of the cartridge image.
  • Have the Arduino check the first two bytes of the cartridge to verify they are correct before reading the entire cartridge.
  • Speed up the transfer of the cartridge data by sending multiple bytes on the same line.

Tuesday, November 25, 2014

Making DPI-Aware Applications in .NET

Since the 1980s, the Microsoft Windows operating system has used a default dots per inch (DPI) setting of 96 pixels per inch (PPI). Until recently this setting was hardly ever changed. A developer could pretty safely assuming any computer running their software would be using a DPI setting of 96 PPI. However, high definition displays are becoming more common. Laptop computers have been using higher DPI displays for a while now (e.g. Apple’s MacBook Pro with Retina display uses a 220 or 227 PPI display, Acer Aspire S7 uses a 220 PPI display, Google Chromebook Pixel uses a 239 PPI display). In October 2014 Apple released their 5120 x 2880 pixel resolution (218 PPI) monitor. Dell has announced it will begin selling an UltraSharp 27 Ultra HD 5K in December 2014 with a 5128 x 2880 pixel resolution (218 PPI). Software developers can no longer assume the computer running their software will be using 96 PPI.

WinForms Applications

By default WinForms applications are not DPI-aware, they assume a 96 PPI. If they are executed on a system with a higher DPI setting, the operating system will virtualize and scale these applications to account for the higher DPI setting. This can cause the applications to suffer from many visual artifacts, including incorrect scaling of UI elements, clipped text, and blurry images. The following are screenshots of a WinForms application running at 96 PPI and 144 PPI:

WinForms Not DPI-Aware - 96 PPI
Figure 1 - WinForms Application 96 PPI


WinForms Not DPI-Aware - 144 PPI
Figure 2 - WinForms Application 144 PPI

The application running at 144 PPI appears blurry, unlike the version running on a 96 PPI display.

Making WinForms Applications DPI-Aware

A WinForms application can be made DPI-aware by adding the following <dpiAware> element to the application’s manifest file:

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
   <asmv3:application>
      <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
         <dpiAware>true</dpiAware>
      </asmv3:windowsSettings>
   </asmv3:application>
</assembly>

Adding this content to the application’s manifest file resolves the blurry text and controls issues seen in the non-DPI-aware WinForms application.

WinForms DPI-Aware - 144 PPI
Figure 3 - WinForms DPI Aware Application 144 PPI

When developing a WinForms application that will be run on machines with various DPI settings, it is wise to test the application with the standard DPI settings (100% (96 PPI); 125% (120 PPI); 150% (144 PPI), and 200% (192 PPI)).

WPF Applications

By default Windows Presentation Foundation (WPF) applications are system DPI-aware. WPF uses device-independent units to facilitate resolution and device independence. WPF automatically scales each device-independent unit based on current system DPI. This results in applications that look correct regardless of the machine’s DPI setting.

WPF DPI-Aware - 96 PPI
Figure 4 - WPF Application 96 PPI


WPF DPI-Aware - 144 PPI
Figure 5 - WPF Application 144 PPI


Per-Monitor DPI-Aware WPF Application

In Windows 8.1 Microsoft added the ability to have different DPI settings for each monitor, instead of a single system DPI setting. WPF applications are system DPI-aware, but they are not monitor DPI-aware. Microsoft has published a detailed article describing how to create a monitor DPI aware application on their website, called “Developing a Per-Monitor DPI-Aware WPF Application” (http://msdn.microsoft.com/en-us/library/windows/desktop/ee308410.aspx https://docs.microsoft.com/en-us/windows/win32/hidpi/declaring-managed-apps-dpi-aware). This is useful for applications that will be used on systems with multiple monitors that have different DPI settings.

Conclusion

High definition displays are here to stay. As application developers we need to ensure our applications look and work properly on traditional 96 PPI systems as well as the new higher PPI system. The newer WPF UI framework handles differing DPI settings by default, but even WinForms applications can be made to work properly on higher PPI systems.

The source code that was used to generate the screenshots for this article can be downloaded at https://static.heironimus.info/blog/dpi-aware/DpiAware.zip.

Saturday, September 20, 2014

Make Your Own Coleco ADAM Audio/Video Cable

TV RF Computer Slide Switch

The Coleco ADAM “Family Computer System” came with an RF switch box used to connect the ADAM to a TV set to channel 3 or 4 (depending on the position of the channel switch on the back of the memory console). This worked, but the image quality using an RF cable can often be fuzzy and the sound can be scratchy.



Near the place where one would connect the TV cable, there is another RCA connector labeled MONITOR that is a Composite Video out. This provides a much better video image, but does not include the audio output.

ADAM Console Back

There is a third connector on the back of the ADAM labeled AUX VIDEO that provides both a composite video signal and an audio signal. Unfortunately Coleco used a 7-pin circular DIN connector, which is hard to find.

This article describes how to create an Audio/Video cable that will allow a Coleco ADAM to be connected to a TV via the TV’s composite video and audio RCA connectors, rather than the antenna input.

What You Need

Prepare the RCA Connectors

Stripped AV Cable

For my cable I used an old camera A/V cable I was not using anymore, but any cable with two male RCA connectors will work. If you do not have a cable like this you can spare, you can create your own using two RCA connectors. Cut the A/V cable at the end opposite the RCA connectors and strip the wires.



 

Connect the Cable to the DIN Connector

The following diagram describes the pin layout of the 7-pin DIN connector as looking at the back of the Coleco ADAM:

ADAM AUX Video Connector

AV Connector Back of ADAM

Before connecting the ends of the A/V cable to the DIN connector, be sure to slide the outer cover of the DIN connector over the A/V cable. If you forget to do this step, you will hate yourself.

AV Cable with DIN Cover

Connect the center wire of the audio cable to pin 1 of the DIN connector. Connect the outer or ground wire of the audio cable to pin 2. Connect the center wire of the composite video cable to pin 3 of the DIN connector. Connect the outer or ground wire of the composite video cable to pin 4 of the DIN connector.

Coleco ADAM AV Cable

Slide the cover over the DIN connector.

Plug the DIN connector into the AUX VIDEO port on the back of the Coleco ADAM memory console and plug the audio and composite video RCA connectors into the TV.

 



Enjoy the sharp image and clear sound of your new connection.

Coleco ADAM


Tuesday, September 09, 2014

Arduino: Classic Joystick to USB Adaptor

If you grew up in the early 1980’s and were into video games, you probably had an Atari 2600, ColecoVision, or similar game console. The controllers or joysticks for each of these systems had a distinct feel that is different from today’s game consoles or PC game controllers. If you find yourself longing to plug your old ColecoVision or Atari 2600 joystick into your modern PC, but are not sure how to go about it, this article is for you. This article describes how to use an Arduino Leonardo (or similar) card to make your classic console joystick look like a USB keyboard.

I designed this Classic Joystick to USB Keyboard Adapter with the ADAMEm ColecoVision and Coleco ADAM emulator in mind (http://www.komkon.org/~dekogel/adamem.html, which can be run in Microsoft Windows using Virtual ADAM http://www.sacnews.net/adamcomputer/downloads/). However it will work with most emulators and any game or program that can use the keyboard as input. I have also tested it with the following emulators:

Default Keyboard Mappings

By default this classic console joystick to USB keyboard adapter uses the following mappings (these are the default mappings used by the ADAMEm emulator):

Joystick Action Keyboard Mapping
Up Up Arrow Key
Down Down Arrow Key
Left Left Arrow Key
Right Right Arrow Key
Left Fire Button Left Alt Key
Right Fire Button Left Ctrl Key
Purple Fire Button Left Shift Key
Blue Fire Button z
Keypad 0 0
Keypad 1 1
Keypad 2 2
Keypad 3 3
Keypad 4 4
Keypad 5 5
Keypad 6 6
Keypad 7 7
Keypad 8 8
Keypad 9 9
Keypad * -
Keypad # =

These mappings can be changed by altering the Arduino sketch file provided (see the Keyboard Keys section).

Atari 2600 Joysticks

Atari 2600 Joystick
The Atari 2600 joysticks are the simplest of the controllers supporter by this adaptor. They have a D-Subminiature Female 9 Position connector. The following table describes the function of each pin:

Pin Number Description
1 Up
2 Down
3 Left
4 Right
6 Fire
8 Ground

ColecoVision Controllers

ColecoVision Controller
ADAM Joystick
ColecoVision Controller
ADAM Controller

ColecoVision Super Action Controller
ColecoVision Super Action Controller

The ColecoVision and ADAM joysticks are much more complicated. In addition to the four direction control stick, they have two fire buttons (or 4 in the case of the Super Action Controller) and a 12 button keypad. Like the Atari 2600 joystick, they have a D-Subminiature Female 9 Position connector, but the pin outs have two different modes. When pin 8 is grounded, pins 1-4 are the four directions and pin 6 is the left fire button. When pin 5 is grounded, pins 1-4 are a keypad scan code and pin 6 is the right fire button. The table below lists the pin outs for the ColecoVision and ADAM controllers.

Pin Number Pin 8 Grounded Pin 5 Grounded
1 Up Bit 0
2 Down Bit 2
3 Left Bit 3
4 Right Bit 1
5 Keypad Mode Keypad Mode
6 Left Fire Right Fire
7 Spinner Spinner
8 Control Stick Mode Control Stick Mode
9 Spinner Spinner

The keypad scan codes are listed below:

Keypad Value
Bit 3 / Pin 3
Bit 2 / Pin 2
Bit 1 / Pin 4
Bit 0 / Pin 1
Decimal Value
Hex Value
1
0
0
1
0
2
2
2
1
0
0
0
8
8
3
0
0
1
1
3
3
4
1
1
0
1
13
D
5
1
1
0
0
12
C
6
0
0
0
1
1
1
7
1
0
1
0
10
A
8
1
1
1
0
14
E
9
0
1
0
0
4
4
0
0
1
0
1
5
5
*
0
1
1
0
6
6
#
1
0
0
1
9
9
Purple Fire Button
0
1
1
1
7
7
Blue Fire Button
1
0
1
1
11
B

The solution I have developed will support Atari 2600 joysticks, ColecoVision and ADAM controllers, and most of the ColecoVision Super Action Controllers. This solution does not currently support the Spinner located below the keypad on the Super Action Controllers.

What You Need

Hardware

The pins of the D-Sub Male 9 Position connector should be connected to the pins of the Arduino Leonardo or Arduino Micro as shown in the table below:

Arduino Pin Male D-Sub Pin Description
2 6 Fire Button
3 1 Up
4 2 Down
5 3 Left
6 4 Right
7 5 Keypad Mode
8 8 Joystick Mode
9 7 Spinner (not used)
10 9 Spinner (not used)

Classic Joystick to USB Adaptor

Software

The Arduino sketch file listed below should be compiled and uploaded into the Arduino Leonardo or Arduino Micro.


// ColecoVision / ADAM Joystick to PC Keyboard Converter
// for the Arduino Leonardo
// 2014-08-24
//----------------------------------------------------------------------------------

// Joystick Pins
const byte gcFirePin = 2;
const byte gcUpPin = 3;
const byte gcDownPin = 4;
const byte gcLeftPin = 5;
const byte gcRightPin = 6;
const byte gcModeAPin = 7;
const byte gcModeBPin = 8;
const byte gcFireMonitorPin = 13;
const byte gcBit0Pin = 3;
const byte gcBit2Pin = 4;
const byte gcBit3Pin = 5;
const byte gcBit1Pin = 6;

// Keyboard Keys
const char gcUpKey = KEY_UP_ARROW;
const char gcDownKey = KEY_DOWN_ARROW;
const char gcLeftKey = KEY_LEFT_ARROW;
const char gcRightKey = KEY_RIGHT_ARROW;
const char gcLeftFireKey = KEY_LEFT_ALT;
const char gcRightFireKey = KEY_LEFT_CTRL;
const char gcPurpleButtonKey = KEY_LEFT_SHIFT;
const char gcBlueButtonKey = 'z';
const char gcAsteriskKey = '-';
const char gcPoundKey = '=';

// Current Frame Joystick Status
byte gLeftFireButton = 0;
byte gRightFireButton = 0;
byte gUp = 0;
byte gDown = 0;
byte gLeft = 0;
byte gRight = 0;
char gNumPadValue = ' ';
byte gPurpleButton = 0;
byte gBlueButton = 0;
byte gBit0 = 0;
byte gBit1 = 0;
byte gBit2 = 0;
byte gBit3 = 0;

// Last Frame Joystick Status
byte gLastLeftFireButton = 0;
byte gLastRightFireButton = 0;
byte gLastUp = 0;
byte gLastDown = 0;
byte gLastLeft = 0;
byte gLastRight = 0;
char gLastNumPadValue = ' ';
byte gLastPurpleButton = 0;
byte gLastBlueButton = 0;

// Line Read Variables
const unsigned int gcThreashold = 4;
unsigned int gLeftFireCount = 0;
unsigned int gRightFireCount = 0;
unsigned int gUpCount = 0;
unsigned int gDownCount = 0;
unsigned int gLeftCount = 0;
unsigned int gRightCount = 0;
unsigned int gBit0Count = 0;
unsigned int gBit1Count = 0;
unsigned int gBit2Count = 0;
unsigned int gBit3Count = 0;

// Frame Variables
const int gcFrameLength = 10;
unsigned int gLoopsPerFrame = 0;
unsigned long gLastFrameStart = 0;

// Shows the status of the joystick.
void ShowJoystickStatus()
{
  // Debug Information
//  Serial.println(gLoopsPerFrame);

  digitalWrite(gcFireMonitorPin, gLeftFireButton | gRightFireButton);
}

void SendKeyboardStateToPc()
{
  if ((gLastNumPadValue != ' ') && (gLastNumPadValue != gNumPadValue))
  {
    Keyboard.release(gLastNumPadValue);
  }
  if ((gNumPadValue != ' ') && (gLastNumPadValue != gNumPadValue))
  {
    Keyboard.press(gNumPadValue);
  }
  
  SendLineStateToPc(gLastLeftFireButton, gLeftFireButton, gcLeftFireKey);
  SendLineStateToPc(gLastRightFireButton, gRightFireButton, gcRightFireKey);
  SendLineStateToPc(gLastUp, gUp, gcUpKey);
  SendLineStateToPc(gLastDown, gDown, gcDownKey);
  SendLineStateToPc(gLastLeft, gLeft, gcLeftKey);
  SendLineStateToPc(gLastRight, gRight, gcRightKey);
  SendLineStateToPc(gLastPurpleButton, gPurpleButton, gcPurpleButtonKey);
  SendLineStateToPc(gLastBlueButton, gBlueButton, gcBlueButtonKey);
}

void SendLineStateToPc(byte lastState, byte currentState, char keyUsedForLine)
{
  if ((lastState == 1) && (currentState == 0))
  {
    Keyboard.release(keyUsedForLine);
  }
  if ((lastState == 0) && (currentState == 1))
  {
    Keyboard.press(keyUsedForLine);
  }
}

unsigned int CheckJoystickLine(byte pin)
{
  return (digitalRead(pin) == LOW);
}

void CheckJoystickLines()
{
  const int cLineDelay = 50;
  
  // Put Joystick in Direction Mode
  digitalWrite(gcModeAPin, HIGH);
  digitalWrite(gcModeBPin, LOW);
  delayMicroseconds(cLineDelay);
  gLeftFireCount += CheckJoystickLine(gcFirePin);
  gUpCount += CheckJoystickLine(gcUpPin);
  gDownCount += CheckJoystickLine(gcDownPin);
  gLeftCount += CheckJoystickLine(gcLeftPin);
  gRightCount += CheckJoystickLine(gcRightPin);

  // Put Joystick in Keypad Mode
  digitalWrite(gcModeAPin, LOW);
  digitalWrite(gcModeBPin, HIGH);
  delayMicroseconds(cLineDelay);
  gRightFireCount += CheckJoystickLine(gcFirePin);
  gBit0Count += CheckJoystickLine(gcBit0Pin);
  gBit1Count += CheckJoystickLine(gcBit1Pin);
  gBit2Count += CheckJoystickLine(gcBit2Pin);
  gBit3Count += CheckJoystickLine(gcBit3Pin);
}

void ResetFrameVariables()
{
  // Copy Current Frame to Last Frame
  gLastNumPadValue = gNumPadValue;
  gLastLeftFireButton = gLeftFireButton;
  gLastRightFireButton = gRightFireButton;
  gLastUp = gUp;
  gLastDown = gDown;
  gLastLeft = gLeft;
  gLastRight = gRight;
  gLastPurpleButton = gPurpleButton;
  gLastBlueButton = gBlueButton;
  
  // Reset Frame Loop Counter
  gLoopsPerFrame = 0;
  
  // Reset Joysick State
  gLeftFireCount = 0;
  gLeftFireButton = 0;
  gRightFireCount = 0;
  gRightFireButton = 0;
  gUpCount = 0;
  gUp = 0;
  gDownCount = 0;
  gDown = 0;
  gLeftCount = 0;
  gLeft = 0;
  gRightCount = 0;
  gRight = 0;
  gBit0Count = 0;
  gBit0 = 0;
  gBit1Count = 0;
  gBit1 = 0;
  gBit2Count = 0;
  gBit2 = 0;
  gBit3Count = 0;
  gBit3 = 0;
  gNumPadValue = ' ';
  gPurpleButton = 0;
  gBlueButton = 0;
}

byte DetermineJoystickLineValue(unsigned int pressCount)
{
    return (pressCount >= gcThreashold);
}

void DetermineJoystickValues()
{
  const char cKeypadValueLookup[16] = {
    ' ', '6', '1', '3', '9', '0', gcAsteriskKey, ' ', 
    '2', gcPoundKey, '7', ' ', '5', '4', '8', ' '};
    
  gLeftFireButton = DetermineJoystickLineValue(gLeftFireCount);
  gRightFireButton = DetermineJoystickLineValue(gRightFireCount);

  gUp = DetermineJoystickLineValue(gUpCount);
  gDown = DetermineJoystickLineValue(gDownCount);
  gLeft = DetermineJoystickLineValue(gLeftCount);
  gRight = DetermineJoystickLineValue(gRightCount);
  
  gBit0 = DetermineJoystickLineValue(gBit0Count);
  gBit1 = DetermineJoystickLineValue(gBit1Count);
  gBit2 = DetermineJoystickLineValue(gBit2Count);
  gBit3 = DetermineJoystickLineValue(gBit3Count);
  
  int keypadCode = (gBit3 << 3) + (gBit2 << 2) + (gBit1 << 1) + gBit0;
  
  gNumPadValue = cKeypadValueLookup[keypadCode];
  
  // Check for SuperAction Controller Buttons.
  if (keypadCode == 7)
  {
    gPurpleButton = 1;
  }
  if (keypadCode == 11)
  {
    gBlueButton = 1;
  }
}

void setup()
{
  // Setup Serial Monitor
//  Serial.begin(19200);
  
  // Setup Joystick Pins
  pinMode(gcFirePin, INPUT_PULLUP);
  pinMode(gcUpPin, INPUT_PULLUP);
  pinMode(gcDownPin, INPUT_PULLUP);
  pinMode(gcLeftPin, INPUT_PULLUP);
  pinMode(gcRightPin, INPUT_PULLUP);
  pinMode(gcModeAPin, OUTPUT);
  pinMode(gcModeBPin, OUTPUT);
  pinMode(gcFireMonitorPin, OUTPUT);
}

void loop()
{
  unsigned long currentTime;
  
  currentTime = millis();
  if (currentTime >= (gLastFrameStart + gcFrameLength))
  {
    // Do Joystick Value Commit Logic
    DetermineJoystickValues();
    
    // Send Values to Monitor
    ShowJoystickStatus();
    
    // Send Keyboad State to the PC
    SendKeyboardStateToPc();
    
    // Reset Frame Variables
    ResetFrameVariables();
  
    // Time to start next frame
    gLastFrameStart = currentTime;
  }
  else
  {
    // Check the value of the input lines and make note of them.
    gLoopsPerFrame++;
    CheckJoystickLines();
  }
}


Setup Function

The setup function is used to initialize the Arduino pins. Pins 2-6 are setup as input pins with pull-up resistors. Pins 7 and 8 are setup as output pins to toggle between Keypad Mode and Controller Mode. Pin 13 is setup as an output pin. On most Arduino boards pin 13 is connected to an LED. This LED is used to monitor the left and right fire buttons.

Loop Function

The main loop function is setup to report the value of the various joystick elements as either keydown or keyup events at a rate defined by the gcFrameLength constant. The main loop will read the state of all input pins in both the Keypad and Controller modes as many times as possible within a frame. If the number of times a pin was low exceeds the gcThreashold value, the pin is considered low for that frame. This is how each input is debounced.

Debugging

The Arduino COM port can be used for debugging by uncommenting out the call to Serial.begin in the setup function and adding debug output to the ShowJoystickStatus function.

Possible Future Enhancements

  • Add support for the ColecoVision Super Action Controller’s Spinner. This would map to the X-axis of the mouse.
  • Add support for ColecoVision Expansion Module #2 (the driving module).
  • Add support for the ColecoVision Roller Controller.
  • Add support for the Atari 2600 Paddle Controllers.
  • Create a version that presents the classic console controller as USB Game Controller instead of a USB Keyboard.

Sunday, August 17, 2014

Missing TFS Alerts

One day our TFS server stopped sending out e-mail alerts.

We first checked and rechecked our Email Alert Settings, but they were all in order.

Our next guess was our server’s access to the SMTP server had been blocked (we are in an environment where only certain machines are allowed to send e-mail messages via SMTP). But we ruled that out when we were able to send out e-mail messages with a simple test application from that server.

After much looking around on the internet and on the TFS server, we came across this error in the Tfs_Configuration.tbl_JobHistory table:

TF400797: Job extension had an unhandled error: Microsoft.TeamFoundation.Framework.Server.IdentityNotFoundException:
TF14045: The identity with TeamFoundationId 6a358919-xxxx-xxxx-xxxx-2c2f342dc379 could not be found.
at Microsoft.TeamFoundation.Framework.Server.Subscription.AfterReadSubscription(TeamFoundationRequestContext requestContext)
at Microsoft.TeamFoundation.JobService.Extensions.Core.NotificationJobExtension.ExpandEvents(TeamFoundationRequestContext requestContext)
at Microsoft.TeamFoundation.JobService.Extensions.Core.NotificationJobExtension.Run(TeamFoundationRequestContext requestContext, TeamFoundationJobDefinition jobDefinition, DateTime jobQueueTime, String& resultMessage)
at Microsoft.TeamFoundation.Framework.Server.JobRunner.ExecuteJob()

We used the following SQL query to find this:

SELECT TOP 1000 [HistoryId]

      ,[JobSource]

      ,[JobId]

      ,[QueueTime]

      ,[StartTime]

      ,[EndTime]

      ,[AgentId]

      ,[Result]

      ,[ResultMessage]

      ,[QueuedReasons]

      ,[QueueFlags]

      ,[Priority]

  FROM [Tfs_Configuration].[dbo].[tbl_JobHistory]

  WHERE ResultMessage IS NOT NULL

  and StartTime > (GETDATE() - 1)

  and Result = 2

  ORDER BY EndTime desc

We looked up the user associated with the GUID listed in the error message and discovered it was related to a user who had left the project a few months earlier. Their user account had been deleted from the Windows domain.

The following TFS bug appears to document the issue we were encountering: https://connect.microsoft.com/VisualStudio/feedback/details/779506/all-tfs-e-mail-notifications-stop-because-of-any-exception-regarding-a-user-alert-or-user-identity (link is no longer valid)

Using the GUID from the error message and the SQL query below, we were able to find the alert subscription that was causing our problems:

select *
from Tfs_PharmacyTechnology.dbo.tbl_EventSubscription
where SubscriberId=
'6A358919-xxxx-xxxx-xxxx-2C2F342DC379'

Using the id field from the results of the query above, we were able to delete the offending subscription using the BisSubscribe.exe command shown below:

> BisSubscribe.exe /unsubscribe /id {id from query above} /collection http://{TFS Server Name}:8080/tfs/{TFS Collection Name}

Once the bad alert subscription was deleted, the other e-mail alert notifications started to be sent.