djtaylor.me

Insert tagline here
BlogThoughtsProjectsResume

The world's shittiest PS/2 emulation

Pub. 2022 May 6

The problem: Julie wants to stop playing Minecraft on her Switch because the Switch version sucks. She wants to start playing on her Macbook, and it runs fine. But she feels more comfortable playing with controllers and joysticks over keyboards and WASD.

She can connect a controller to her Macbook, but this has its own issues. The first one is that we'd have to buy a new fancy controller, since Apple apparently only has a supported driver for bluetooth xbox controllers, not the wired ones that I have. And since Big Sur, the only major community driver was killed. But even if a controller was just purchased, there's still an input problem. Because despite Minecraft famously being written using LWJGL, and having several console ports that use controllers, Minecraft Java doesn't have controller support. There are apparently mods that fix this, though, so that's an option as well.

But despite all of those solutions being pretty doable, that's not what we're doing. Fuck those options.

I decided that it would be better (and more fun for myself) if we just made a controller that exposed itself as a keyboard when you plugged it in. So normally the thing you would do for older games and shit operating systems like Windows and Linux and MacOS is that you would have separate programs that intercept your controller button presses and joystick motions and then emit keyboard keypresses. But the idea here is that the controller can just directly emit keyboard presses. There are many controllers out there that do this actually. Wasn't the Steam Controller one of them? Anyways, the other important fact is that games normally allow you to rebind all of their keys these days, so you don't actually need any shit drivers or shit bloatware that those other guys have that allows you to change what keys the buttons and joystick emit. The joystick can just emit A, B, C, D, E, F, etc. and the user can rebind them to forward, left, right, back, jump, crouch, etc in your game of choice. I'm not sure what you'd call this. I'm calling this keyboard emulation, but that doesn't seem like the right word.

Julie eventually settled on a dpad instead of a joystick after we went to a retro-like place and felt the dpad of a knock-off genesis controller. I myself never really liked the genesis dpad, it always felt too wobbly for me. But Julie thought it felt good, so we took and stripped it for its buttons and dpad. That part, and the 3d printing of the housing is still being done so maybe I'll post an update about that when it happens. But this is a post about the PS/2 emulation.

(Unrelated, but there used to be a store near where I grew up that sold old consoles and games and gamesharks, as well as newer consoles and games. I mostly knew about them because they had a popular game rental system and a service where you could bring in scratched up CDs and the like and they would polish the scratches out. It left my area maybe around 2009, but I think they had one of those stupid looking logos that could only belong to a franchise. I can't remember the name of the store and I can't find anything online that rings a bell. I'd like to find them again one day, if they still exists)

I had an old Arduino Mega laying around, and I remember reading about the PS/2 and USB HID protocols and remember that PS/2 looked fairly simple and low frequency. The Arduino can't output a user generated USB signal, probably to make sure that the USB programming functionality never breaks. But that's no problem, because I also happen to have one of these PS/2 to USB adapters sitting around as well! I have a feeling that this little device saved me lots of time and effort. I don't know this because I don't have my tools here to open it up, but I think this device is actually doing the work to convert the PS/2 signal to a USB one, and not just wiring the DATA to D+ and the CLOCK to D- or whatever the really simple converters do. And I believe that this little converter is why I don't have to deal with lots of different message types. At least, this is what this random internet page seems to imply. Also, I've read elsewhere that hot plugging a PS/2 keyboard that just has its wires switched around doesn't work on the keyboard controllers on some motherboards.

I mostly used two resources to make it: this pdf, which seems to itself be a Print-Webpage-To-PDF that someone did of a website that doesn't exist anymore. And this one, a manual for a MC68HC908JB8 chip.

My setup looked like this. Fair warning, I'm the least experienced person in any room when it comes to electronics.

Here I'm just testing two pushbuttons, one types a w and the other an s. Also, I really need to clean my desk every now and again, christ.
Actually I connected to the 3.3V pin on the Arduino instead of Vin since I'm a scardy-cat

You can see in the picture my general setup. I direct the DATA and CLOCK lines to the open collectors on the breadboard and direct the data straight into the power plug. Also, since I'm powering the board through the power plug the USB serial connection is disabled, so I just made do with those 8 LEDs to the right for debugging. They're not different colors for any special reason, I just ran out. I'm sure there's a way to power the board through the USB port, and also be able to read/write data to the PS/2 signal but I really wasn't comfortable trying that. I simply don't have the electronics know-how.

The programming side was pretty simple. I know literally nothing about electronics, and it took two evenings. And most of the second evening was just just trying to debug the first bullet point problem with the code listed below.

In the end, though, none of the code ended up being used. We were mostly doing this so that we didn't have to order something and have to wait for something that might not work. But we did actually have to end up ordering another board and some more diodes, and it turns out that the Arduino Micro already has the ability to send USB packets out. I'm not sure why the Mega doesn't have it, seeing as how the Micro proves it's definitely possible. In addition, the Micro even has a library to emulate keyboards and mice! So since we were ordering a new board for her anyways, we just decided to get the Micro and chuck the code. However I'm in the process of building a custom keyboard for myself, and I have no such scruples about using my own shitty code over more mature "solutions."

The code

Because after a short online search I didn't find anything similar to this, I'm posting it here as a starting point for others. I'm a huge fan of the WTFPL, found here, so that's what the license is. There are a number of problems with this code. Here are a sampling for your enjoyment:

int clock_in = 50;
int clock_out = 40;
int data_in = 51;
int data_out = 41;

int debug_pins[8] = {2,3,4,5,8,9,10,11};

int w_pin = 30;
bool w_down = false;

// The output byte ring buffer.
u8 buffer[256] = {0};
int num_bytes = 0;
int buffer_cursor = 0;

void setup() {

  pinMode(clock_in, INPUT);
  pinMode(data_in, INPUT);

  pinMode(clock_out, OUTPUT);
  pinMode(data_out, OUTPUT);

  for (int i = 0; i < 8; ++i) {
    pinMode(debug_pins[i], OUTPUT);
  }

  pinMode(w_pin, INPUT);
  w_down = false;

  buffer[num_bytes % 256] = 0xaa;
  num_bytes += 1;

  // Sending the initial BAT code here without checks, since the CLOCK line can be a little fritzy at this point in the process.
  {
    bool bits[11];
    bits[0] = 0;
    bits[1] = ((buffer[buffer_cursor % 256] >> 0) & 0x01);
    bits[2] = ((buffer[buffer_cursor % 256] >> 1) & 0x01);
    bits[3] = ((buffer[buffer_cursor % 256] >> 2) & 0x01);
    bits[4] = ((buffer[buffer_cursor % 256] >> 3) & 0x01);
    bits[5] = ((buffer[buffer_cursor % 256] >> 4) & 0x01);
    bits[6] = ((buffer[buffer_cursor % 256] >> 5) & 0x01);
    bits[7] = ((buffer[buffer_cursor % 256] >> 6) & 0x01);
    bits[8] = ((buffer[buffer_cursor % 256] >> 7) & 0x01);
    bits[9] = 1 - (bits[1] + bits[2] + bits[3] + bits[4] + bits[5] + bits[6] + bits[7] + bits[8])%2;
    bits[10] = 1;
    for (int i = 0; i < 11; ++i) {
      digitalWrite(data_out, 1-bits[i]);
      digitalWrite(clock_out, 1);
      delayMicroseconds(100);
      digitalWrite(clock_out, 0);
      delayMicroseconds(100);
    }
  }
}

void write_debug(u8 b) {
  digitalWrite(debug_pins[0], (b >> 0) & 0x01);
  digitalWrite(debug_pins[1], (b >> 1) & 0x01);
  digitalWrite(debug_pins[2], (b >> 2) & 0x01);
  digitalWrite(debug_pins[3], (b >> 3) & 0x01);
  digitalWrite(debug_pins[4], (b >> 4) & 0x01);
  digitalWrite(debug_pins[5], (b >> 5) & 0x01);
  digitalWrite(debug_pins[6], (b >> 6) & 0x01);
  digitalWrite(debug_pins[7], (b >> 7) & 0x01);
}

void loop() {
  
  if (digitalRead(clock_in) == HIGH && digitalRead(data_in) == HIGH &&
      (buffer_cursor != num_bytes)) {

    u8 message = buffer[buffer_cursor % 256];

    bool bits[11];
    bits[0] = 0;
    bits[1] = ((message >> 0) & 0x01);
    bits[2] = ((message >> 1) & 0x01);
    bits[3] = ((message >> 2) & 0x01);
    bits[4] = ((message >> 3) & 0x01);
    bits[5] = ((message >> 4) & 0x01);
    bits[6] = ((message >> 5) & 0x01);
    bits[7] = ((message >> 6) & 0x01);
    bits[8] = ((message >> 7) & 0x01);
    bits[9] = 1 - (bits[1] + bits[2] + bits[3] + bits[4] + bits[5] + bits[6] + bits[7] + bits[8])%2;
    bits[10] = 1;
    for (int i = 0; i < 11; ++i) {
      digitalWrite(data_out, 1-bits[i]);
      digitalWrite(clock_out, 1);
      delayMicroseconds(100);
      digitalWrite(clock_out, 0);
      #if 1
      delayMicroseconds(100);
      #else
      for (int j = 0; j < 5; ++j) {
        delayMicroseconds(20);
        if (digitalRead(clock_in) == LOW) {
          // The host is taking over the line! Just abort this message and try again later
          write_debug(0x20);
          goto packet_write_fail;
        }
      }
      #endif
    }

    buffer_cursor += 1;

    // Release control over the lines so that thay are in the correct state to send/recieve more
    packet_write_fail:
    digitalWrite(data_out, 0);
    digitalWrite(clock_out, 0);
  }

  // Is the host trying to send data?
  {
    if (digitalRead(data_in) == LOW) {
      int bits[10] = {0};

      for (int i = 0; i < 10; ++i) {
        digitalWrite(clock_out, 1);
        delayMicroseconds(100);
        digitalWrite(clock_out, 0);
        bits[i] = digitalRead(data_in);
        delayMicroseconds(100);
      }
      digitalWrite(data_out, 1);
      digitalWrite(clock_out, 1);
      delayMicroseconds(100);
      digitalWrite(data_out, 0);
      digitalWrite(clock_out, 0);

      u8 message = (bits[1]) | (bits[2] << 1) | (bits[3] << 2) | (bits[4] << 3) |
        (bits[5] << 4) | (bits[6] << 5) | (bits[7] << 6) | (bits[8] << 7);

      write_debug(message);

      {
        if (message == 0xff) {
          buffer[num_bytes % 256] = 0xaa;
        } else {
          buffer[num_bytes % 256] = 0xfa;
          ++num_bytes;
        }
      }
    }
  }

  if (!w_down && digitalRead(w_pin) == HIGH) {
    buffer[num_bytes % 256] = 0x1d;
    ++num_bytes;
    w_down = true;
  }
  if (w_down && digitalRead(w_pin) == LOW) {
    buffer[num_bytes % 256] = 0xf0;
    ++num_bytes;
    buffer[num_bytes % 256] = 0x1d;
    ++num_bytes;
    w_down = false;
  }

  // Slow it all down a bit to 50kHz
  delayMicroseconds(20);
}
Written by Daniel Taylor.
Email: culdevu@gmail.com

© 2022 by Daniel Taylor