Sega Genesis controllers and Arduino revisited

NOTICE: This research and implementation in this post is not 100% correct. Please check out my SegaController Arduino library for better code and How To Read Sega Controllers for details on how it works.

Version 1.0

A couple of months ago I detailed the process by which I used an Arduino Uno to read the button presses of a pair of Sega Genesis controllers. It was my first bit of micro-controller programming, and it has exposed me to a whole new world of possibilities for hardware hacking. So it is with some certain future projects in mind that I’ve revisited and updated that initial code.

Version 1.1

Other than some minor code cleanup, the only major feature-add to the core of the sketch is to support reading the “Mode” button on six-button controllers (thanks to a comment from soe for that). The other change, now that I’ve acquired an Arduino Leonardo, is to create a fork of the sketch which reports the button presses as keyboard key presses via the ATmega32u4 chip’s ability appear as a USB keyboard.

Anyway, here’s the updated code (for reporting over the Serial connection):

/*
 * Sega Controller Reader
 * Author: Jon Thysell <thysell@gmail.com>
 * Version: 1.1
 * Date: 9/29/2014
 *
 * Reads buttons presses from Sega Genesis 3/6 button controllers
 * and reports their state via the Serial connection. Handles hot
 * swapping of controllers and auto-switches between 3 and 6 button
 * polling patterns.
 *
 */

const int PLAYERS = 2;

// Controller Button Flags
const int ON = 1;
const int UP = 2;
const int DOWN = 4;
const int LEFT = 8;
const int RIGHT = 16;
const int START = 32;
const int A = 64;
const int B = 128;
const int C = 256;
const int X = 512;
const int Y = 1024;
const int Z = 2048;
const int MODE = 4096;

// Controller DB9 Pin 7 Mappings
const int SELECT[] = { 8, 9 };

typedef struct
{
  int player;
  int pin;
  int lowFlag;
  int highFlag;
  int pulse3Flag;
} input;

// Controller DB9 Pin to Button Flag Mappings
// First column is the controller index, second column
// is the Arduino pin that the controller's DB9 pin is
// attached to, remaing columns are the button flags
input inputMap[] = {
  { 0,  2,  UP,    UP,     Z    }, // P0 DB9 Pin 1
  { 0,  3,  DOWN,  DOWN,   Y    }, // P0 DB9 Pin 2
  { 0,  4,  ON,    LEFT,   X    }, // P0 DB9 Pin 3
  { 0,  5,  ON,    RIGHT,  MODE }, // P0 DB9 Pin 4
  { 0,  6,  A,     B,      0    }, // P0 DB9 Pin 6
  { 0,  7,  START, C,      0    }, // P0 DB9 Pin 9
  { 1,  A0, UP,    UP,     Z    }, // P1 DB9 Pin 1
  { 1,  A1, DOWN,  DOWN,   Y    }, // P1 DB9 Pin 2
  { 1,  A2, ON,    LEFT,   X    }, // P1 DB9 Pin 3
  { 1,  A3, ON,    RIGHT,  MODE }, // P1 DB9 Pin 4
  { 1,  A4, A,     B,      0    }, // P1 DB9 Pin 6
  { 1,  A5, START, C,      0    }  // P1 DB9 Pin 9
};

// Controller State
int currentState[] = { 0, 0 };
int lastState[] = { -1, -1 };

// Default to three-button mode until six-button connects
boolean sixButtonMode[] = { false, false };

void setup()
{
  // Setup input pins
  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    pinMode(inputMap[i].pin, INPUT);
    digitalWrite(inputMap[i].pin, HIGH);
  }

  // Setup select pins
  for (int i = 0; i < PLAYERS; i++)
  {
    pinMode(SELECT[i], OUTPUT);
    digitalWrite(SELECT[i], HIGH);
  }

  Serial.begin(9600);
}

void loop()
{
  readButtons();
  sendStates();
}

void readButtons()
{
  for (int i = 0; i < PLAYERS; i++)
  {
    resetState(i);
    if (sixButtonMode[i])
    {
      read6buttons(i);
    }
    else
    {
      read3buttons(i);
    }
  }
}

void resetState(int player)
{
  currentState[player] = 0;
}

void read3buttons(int player)
{
  // Set SELECT LOW and read lowFlag
  digitalWrite(SELECT[player], LOW);

  delayMicroseconds(20);

  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].lowFlag;
    }
  }

  // Set SELECT HIGH and read highFlag
  digitalWrite(SELECT[player], HIGH);

  delayMicroseconds(20);

  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].highFlag;
    }
  }

  // When a six-button first connects, it'll spam UP and DOWN,
  // which signals the game to switch to 6-button polling
  if (currentState[player] == (ON | UP | DOWN))
  {
    sixButtonMode[player] = true;
  }
  // When a controller disconnects, revert to three-button polling
  else if ((currentState[player] & ON) == 0)
  {
    sixButtonMode[player] = false;
  }

  delayMicroseconds(20);
}

void read6buttons(int player)
{
  // Poll for three-button states twice
  read3buttons(player);
  read3buttons(player);

  // After two three-button polls, pulse the SELECT line
  // so the six-button reports the higher button states
  digitalWrite(SELECT[player], LOW);
  delayMicroseconds(20);
  digitalWrite(SELECT[player], HIGH);

  for(int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].pulse3Flag;
    }
  }

  delayMicroseconds(1000);
}

void sendStates()
{
  // Only report controller states if at least one has changed
  boolean hasChanged = false;

  for (int i = 0; i < PLAYERS; i++)
  {
    if (currentState[i] != lastState[i])
    {
      hasChanged = true;
    }
  }

  if (hasChanged)
  {
    for (int i = 0; i < PLAYERS; i++)
    {
      Serial.print((currentState[i] & ON) == ON ? "+" : "-");
      Serial.print((currentState[i] & UP) == UP ? "U" : "0");
      Serial.print((currentState[i] & DOWN) == DOWN ? "D" : "0");
      Serial.print((currentState[i] & LEFT) == LEFT ? "L" : "0");
      Serial.print((currentState[i] & RIGHT) == RIGHT ? "R" : "0");
      Serial.print((currentState[i] & START) == START ? "S" : "0");
      Serial.print((currentState[i] & A) == A ? "A" : "0");
      Serial.print((currentState[i] & B) == B ? "B" : "0");
      Serial.print((currentState[i] & C) == C ? "C" : "0");
      Serial.print((currentState[i] & X) == X ? "X" : "0");
      Serial.print((currentState[i] & Y) == Y ? "Y" : "0");
      Serial.print((currentState[i] & Z) == Z ? "Z" : "0");
      Serial.print((currentState[i] & MODE) == MODE ? "M" : "0");

      Serial.print((i == 0) ? "," : "\n");
      lastState[i] = currentState[i];
    }
  }
}

As with the previous version, all you have to do is upload the sketch and connect the controller’s DB9 pins to the Arduino following the mapping in inputMap in the code.

Now, if you have an Arduino Leonardo (or compatible board), and want the controller button presses to map to keyboard keys instead, you can upload the following sketch:

/*
 * Sega Controller Reader (Keyboard)
 * Author: Jon Thysell <thysell@gmail.com>
 * Version: 1.1
 * Date: 9/29/2014
 *
 * Reads buttons presses from Sega Genesis 3/6 button controllers
 * and reports their state via keyboard button presses. Handles hot
 * swapping of controllers and auto-switches between 3 and 6 button
 * polling patterns.
 *
 */

const int PLAYERS = 2;

// Controller Button Flags
const int ON = 1;
const int UP = 2;
const int DOWN = 4;
const int LEFT = 8;
const int RIGHT = 16;
const int START = 32;
const int A = 64;
const int B = 128;
const int C = 256;
const int X = 512;
const int Y = 1024;
const int Z = 2048;
const int MODE = 4096;

// Controller DB9 Pin 7 Mappings
const int SELECT[] = { 8, 9 };

typedef struct
{
  int player;
  int pin;
  int lowFlag;
  int highFlag;
  int pulse3Flag;
} input;

// Controller DB9 Pin to Button Flag Mappings
// First column is the controller index, second column
// is the Arduino pin that the controller's DB9 pin is
// attached to
input inputMap[] = {
  { 0,  2,  UP,    UP,     Z    }, // P0 DB9 Pin 1
  { 0,  3,  DOWN,  DOWN,   Y    }, // P0 DB9 Pin 2
  { 0,  4,  ON,    LEFT,   X    }, // P0 DB9 Pin 3
  { 0,  5,  ON,    RIGHT,  MODE }, // P0 DB9 Pin 4
  { 0,  6,  A,     B,      0    }, // P0 DB9 Pin 6
  { 0,  7,  START, C,      0    }, // P0 DB9 Pin 9
  { 1,  A0, UP,    UP,     Z    }, // P1 DB9 Pin 1
  { 1,  A1, DOWN,  DOWN,   Y    }, // P1 DB9 Pin 2
  { 1,  A2, ON,    LEFT,   X    }, // P1 DB9 Pin 3
  { 1,  A3, ON,    RIGHT,  MODE }, // P1 DB9 Pin 4
  { 1,  A4, A,     B,      0    }, // P1 DB9 Pin 6
  { 1,  A5, START, C,      0    }  // P1 DB9 Pin 9
};

typedef struct
{
  int player;
  int flag;
  char key;
} output;

// Controller Button Flag to Keyboard Mappings
// First column is the controller index, second column
// is the button flag, third is keyboard key
output outputMap[] = {
  { 0, UP,    KEY_UP_ARROW },
  { 0, DOWN,  KEY_DOWN_ARROW },
  { 0, LEFT,  KEY_LEFT_ARROW },
  { 0, RIGHT, KEY_RIGHT_ARROW },
  { 0, START, KEY_RETURN },
  { 0, A,     'z' },
  { 0, B,     'x' },
  { 0, C,     'c' },
  { 0, X,     'a' },
  { 0, Y,     's' },
  { 0, Z,     'd' },
  { 0, MODE,  'q' },
  { 1, UP,    'i' },
  { 1, DOWN,  'k' },
  { 1, LEFT,  'j' },
  { 1, RIGHT, 'l' },
  { 1, START, 't' },
  { 1, A,     'v' },
  { 1, B,     'b' },
  { 1, C,     'n' },
  { 1, X,     'f' },
  { 1, Y,     'g' },
  { 1, Z,     'h' },
  { 1, MODE,  'r' }
};

// Controller State
int currentState[] = { 0, 0 };
int lastState[] = { -1, -1 };

// Default to three-button mode until six-button connects
boolean sixButtonMode[] = { false, false };

void setup()
{
  // Setup input pins
  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    pinMode(inputMap[i].pin, INPUT);
    digitalWrite(inputMap[i].pin, HIGH);
  }

  // Setup select pins
  for (int i = 0; i < PLAYERS; i++)
  {
    pinMode(SELECT[i], OUTPUT);
    digitalWrite(SELECT[i], HIGH);
  }

  Keyboard.begin();
}

void loop()
{
  readButtons();
  sendStates();
}

void readButtons()
{
  for (int i = 0; i < PLAYERS; i++)
  {
    resetState(i);
    if (sixButtonMode[i])
    {
      read6buttons(i);
    }
    else
    {
      read3buttons(i);
    }
  }
}

void resetState(int player)
{
  currentState[player] = 0;
}

void read3buttons(int player)
{
  // Set SELECT LOW and read lowFlag
  digitalWrite(SELECT[player], LOW);

  delayMicroseconds(20);

  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].lowFlag;
    }
  }

  // Set SELECT HIGH and read highFlag
  digitalWrite(SELECT[player], HIGH);

  delayMicroseconds(20);

  for (int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].highFlag;
    }
  }

  // When a six-button first connects, it'll spam UP and DOWN,
  // which signals the game to switch to 6-button polling
  if (currentState[player] == (ON | UP | DOWN))
  {
    sixButtonMode[player] = true;
  }
  // When a controller disconnects, revert to three-button polling
  else if ((currentState[player] & ON) == 0)
  {
    sixButtonMode[player] = false;
  }

  delayMicroseconds(20);
}

void read6buttons(int player)
{
  // Poll for three-button states twice
  read3buttons(player);
  read3buttons(player);

  // After two three-button polls, pulse the SELECT line
  // so the six-button reports the higher button states
  digitalWrite(SELECT[player], LOW);
  delayMicroseconds(20);
  digitalWrite(SELECT[player], HIGH);

  for(int i = 0; i < sizeof(inputMap) / sizeof(input); i++)
  {
    if (inputMap[i].player == player && digitalRead(inputMap[i].pin) == LOW)
    {
      currentState[player] |= inputMap[i].pulse3Flag;
    }
  }

  delayMicroseconds(1000);
}

void sendStates()
{
  for (int i = 0; i < sizeof(outputMap) / sizeof(output); i++)
  {
    int last = (lastState[outputMap[i].player] & outputMap[i].flag);
    int current = (currentState[outputMap[i].player] & outputMap[i].flag);

    if (last != current)
    {
      if (current == outputMap[i].flag)
      {
        Keyboard.press(outputMap[i].key);
      }
      else
      {
        Keyboard.release(outputMap[i].key);
      }
    }
  }

  for (int i = 0; i < PLAYERS; i++)
  {
    lastState[i] = currentState[i];
  }
}

If you want to change the button to key mapping, simply update outputMap in the code. You can use any key on the keyboard, including special keys and modifiers.

I hope this is a worthy updated to the sketch – it was a great delight to test it by playing emulated games on my laptop with real Genesis controllers. It worked wonderfully, and I can’t wait to get this integrated into a bigger project.

So happy hacking and stay tuned for more!

/jon

Update 26-JUN-2017: The source is now available on GitHub here.

Update 28-JUN-2017: I’ve announced a newer, stable, reusable version at Introducing the SegaController Arduino library.

22 thoughts on “Sega Genesis controllers and Arduino revisited

    1. I’m sorry if i sound stupid with this question but understood most the code but the mapping portion of the Leonardo sketch doesn’t really help me with the actually hocking of the pins from the db9 to the arduiono leonardo as all the entries on the controller side are zeros? Is their a visual guide or picture i could use?

  1. Thank you very much for this! I’m using it right now, just played some Mortal Kombat Trilogy matches on PC 🙂

    I’m having issues though, not sure if it’s due to noise. My controllers are cheap clones I bought years ago for this purpose (although the idea was to hack them to direct key presses and wire them to an old keyboard), so noise is a real possibility.

    The problem is that, from time to time, the controller goes crazy and just stays that way, sending constant button presses. Reset doesn’t help. Maybe, due to noise or to the fact that physically it is possible to press both up and down on these clone controllers?

    Do you think I should add some pull up/down resistors to the circuit?

    Thanks!!

    1. Glad the circuit works for you. The input pins are already pulled high with internal resistors, so I doubt that’s a problem. Is it possible there’s a short in how you’ve wired your circuit? Does it work with official controllers? My guess would be that there’s something off in the clone controllers. You could try modifying serial sketch to report the direct pin readings (not the button mappings) and see if maybe a button is getting stuck or shorting.

  2. Id love to see this with direct outputs instead of USB – for use in jamma projects – is that possible? I know the arduino has less outputs than you’d need…but seeing as its just direct outs, could you use a standalone output AVR?

    1. It wouldn’t be that hard to modify the code to set output pins high and low with button presses – if you just wanted one controller you could probably repurpose the remaining pins on the one Arduino.

  3. Hi! Thank you for your work!
    If I connect my 6-button joysticks (not original, but works fine on the original Mega Drive 1), then output to the serial:

    +UD000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +00000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +00000000+UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    +UD0000000000,-000000000000
    +000000000000,-000000000000
    ………….
    And to infinity loop

    But if i do that:

    boolean sixButtonMode[] = { true, true };

    ……………………

    void readButtons()
    {
    for (int i = 0; i < PLAYERS; i++)
    {
    resetState(i);
    //if (sixButtonMode[i])
    //{
    read6buttons(i);
    //}
    //else
    //{
    //read3buttons(i);
    // }
    }
    }

    It works perfectly.

    Also i found this:
    http://segaretro.org/Control_Pad_(Mega_Drive)
    http://segaretro.org/Six_Button_Control_Pad_(Mega_Drive)

    1. I only had a few controllers to actually test (all official), but what you’re seeing is the six-button joystick trying to tell the game to switch to six-button polling (why you see Up and Down at the same time). My sketch detects that and switches to six-button polling automatically and but remember it also checks this on behalf of two controllers, one after another. For some reason the controller is not happy with the timing and so keeps spamming the game to switch.

      Everything here is timing based, and the various delay values I used were found via trial and error, tested with official controllers, and unfortunately, only one controller connected at a time.

      My guess is that the timing values I settled on, that worked with the chips in the official controllers, are slightly out of sync with the chip in the unofficial controller.

      What you’ve done is disabled the auto-detect and forced six-button polling for both players, which in effect has altered the timing. While this works, it means you lose the benefits of the auto-detect, and three button controllers will report random incorrect button presses.

      A different solution would be to alter the timings by experimenting with the delay values (especially the final delay in read6buttons) to get everything to work without losing anything. Alternatively if you’re only ever playing single player, you could set PLAYERS = 1, but that might be too drastic a timing change.

      Of course, if you have everything working and aren’t planning on supporting other setups, then you can just keep what you have. 🙂

      Let me know if you try changing the delay values and what numbers you come up with. I’d love to have a sketch that supports the variances in controller chips out there.

      1. OK. final delay in read6button from 1400 and up work for me ))) (1350 not stable)

        Now i wait arduino micro to connect a pads to retropi and PC. (if there was a code interprets the serial output to a the pad commands can use and UNO and nano. Retropi have a driver responsible for the DB9 Pads, but it does not work correctly with my controllers in 6 Button Mode (in 3 button mode all ok) and there is need to specify the button mode and number of players before connecting joysticks )

  4. Hi Jon,
    First thanks for your efforts here. I have used it with success with a Pro Micro. I say success but actually it is partial success. I bought some six button controllers off eBay which unfortunately appear to be about the only ones you can get these days and I cant get the six buttons to work. I have a real megadrive and I used these with Mortal Combat and the six buttons do appear to work ok with this game.

    In three button mode no problems. If I force six button mode I see what you described in your first post where I get multiple key presses with up, down etc.

    I have tried looking at exactly what is returned when the controller is first plugged in and I dont seem to see the UP/DOWN combo. I have tried adding extra high low pulses to get the four frames described here: http://segaretro.org/Six_Button_Control_Pad_(Mega_Drive)

    I have tried varying the timing in small increments and grossly but it seems to make no difference. Indeed the three buttons continue to work no matter what timing I use?

    So I will keep tinkering but I thought I would say thanks and also see if you or anyone else reading this has any ideas or experience with this crappy controller.

    The controller link follows. I pulled it apart and there is nothing in there except for what I assume to be an IC under a black circle?

    http://www.ebay.com.au/itm/6-Button-Game-Console-Controller-PAD-For-SEGA-MEGA-Drive-Megadrive-MD-Genesis-/191203087596?pt=LH_DefaultDomain_15&hash=item2c849794ec#ht_5431wt_916

  5. Hi Jon,
    Thanks for creating this vividly detailed document. I have been exploring and experimenting with creating novel ways to interact with technology. I found this tutorial at a perfect junction in my tinkerings.
    I built a version of this project by repurposing an obsolete db9 data switching box that houses the Leonardo snugly and provides a rock Solid housing. I am much more of a ‘hardware guy,’ but am working my
    Way through learning the software/programming.

    The gist:
    could you show me how to have the Leonardo output the HID device as a MIDI (or secondarily as joysticks.)
    I will share photos of my build as well.
    Thanks again

  6. Hi Jon,

    This is really great stuff, thank you for this. I am using this tool to troubleshoot some erratic behavior by various 6-button PCB’s, which seems to be triggered by removing them from their native housing and wiring to external controls. Hoping you can look at the info and see some clear fix, because it’s rather frustrating.

    Issue occurs on a MK-1470 official 6-button controller, and on a SG-6 3rd party controller. The issue does not occur on a MK-1627 (MK-1627 works perfectly when wiring up to external controls). Others have reported the same issues across multiple PCB’s, although I don’t know what PCB’s those are, and I presume these are also the issues Egar reports above.

    Once wired up to external controls (wires soldered to pads on board, then connected appropriately to external buttons and joystick), the PCB will “ghost” x and y button presses randomly and intermittently while in game. I can sit there with my hands nowhere near the controller, and a x or y button press will occur randomly and intermittently, with no apparent regular frequency. In the case of the MK-1470, it would also start up in 3-button mode randomly and intermittently, without the mode button being pressed. The SG-6 does not appear to have that problem.

    I tried replicating the issue using your tool. I did not yet have it when working on the MK-1470, but have been testing with the SG-6. I had to change the delay timing to 1800 in order to get away from U/D spamming, but once the timing is set to 1800 (also tested up to 2000, with success), the controller acts perfectly normal. I can do everything in my power to try to trigger the x/y ghosting with no sucess. I let it sit for minutes, wherein it would have ghosted an x or a y in-game, and it sits there happily doing nothing.

    Yes, i have checked all my soldering, and been around all my work with the multimeter, there is no mechanical reason that I can find for this occurring on multiple PCB’s.

    I do notice a possible correlation that the ghosting buttons in question are all on the 7th cycle, which is immediately following 6-button detection on the 6th cycle. Maybe that has something to do with it? I’ve puzzled over this for quite some time, with a few destroyed controllers in the process. Hoping your wisdom will see something more deeply in this than my novice understanding does. 🙂

    1. Hi Chris,

      It’s been a while since I looked at this, and honestly I can’t remember the types of six-button controllers I used when I first built this sketch (I think they were official Japanese controllers).

      What I do know now thanks to pages like http://segaretro.org/Six_Button_Control_Pad_(Mega_Drive) is that there are actually eight cycles to read for the controller to be read properly and that maybe my timing approach just happens to work often enough to be playable. 😦

      If I were to revisit this code (other than posting it to GitHub for easier versioning) I would probably try re-implementing the logic based on the info on Sega Retro.

      /jon

  7. Hello, I loved your work but there is something that I still do not understand, what program or how do you make a sega emulator or any other recognize the buttons that are pressed?

    1. There’s a SegaControllerKeyboardReader example that converts the button presses into keyboard keys. Then you config your emulator to respond to those keys.

Leave a Reply to Benjamin Martin Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.