[SOLVED] Arduino as PS/2 keyboard - Reliability (~5% of commands are dropped).

Dear Sirs,

I have started a thread in Project Guidance section ([SOLVED] Arduino as PS/2 keyboard - issue (5-10% of commands are dropped) - Project Guidance - Arduino Forum), some time ago. May I try this question here?

I’ll try to be brief. I am trying to consolize an MS-DOS laptop by emulating PS/2 keyboard, i.e I'm trying to make this a thing:

To achieve it I’ve soldered this thing (based on Pro Micro, Atmega32u4) to act as a PS/2 keyboard:

I’ve made a code based on ps2dev library from here https://github.com/Harvie/ps2dev:

#include <Keyboard.h> // Emulate USB keyboard
#include <ps2dev.h> // Emulate a PS/2 device
PS2dev keyboard(A3, A2); // Clock, Data

// Define SEGA gamepad pins
#define UP_OR_Z 9
#define DOWN_OR_Y 7
#define LEFT_OR_X 5
#define RIGHT_OR_MODE 4
#define B_OR_A 8
#define SEL 6
#define C_OR_START 3

// Buttons database follows:
byte PS2_key_code[102][2][3] = { // Array [Button No.][MAKE or BREAK][byte No.]
  {{0x14}, {0xF0, 0x14}}, // 0 - L CTRL
  {{0x12}, {0xF0, 0x12}}, // 1 - L SHFT
// ...other 98 keys are omitted due to 9000 characters forum limitation...
  {{0xE0, 0x2F}, {0xE0, 0xF0, 0x2F}}, // 101 - APPS
};

byte USB_key_code[102] = { // Array [Button No.]
  0x80, // 0 - L CTRL
  0x81, // 1 - L SHFT
// ...other 98 keys are omitted due to 9000 characters forum limitation...
  0xED, // 101 - APPS
};

unsigned long sega_timer = 0; // Needed to track delay after SEGA checked its buttons

byte sega_pin[] = {UP_OR_Z, DOWN_OR_Y, LEFT_OR_X, RIGHT_OR_MODE, B_OR_A, C_OR_START};
// Buttons UP, DOWN, LEFT, RIGHT, B, C, A, START, Z, Y, X, MODE
boolean button_matrix[12]; // Array shows which buttons are pressed
boolean prev_button_matrix[12]; // Cache Array to pick up updates
byte key_press[] = {83, 82, 81, 80, 2, 0, 48, 45, 31, 32, 33, 44}; // List buttons to press

void segaRead() {
  digitalWrite(SEL, LOW);   // Set SEL to LOW to read additional buttons
  delayMicroseconds(20);
  digitalWrite(SEL, HIGH);
  delayMicroseconds(20);
  digitalWrite(SEL, LOW);
  delayMicroseconds(20);

  // Read START and A
  for (byte i = 0; i < 2; i++) {
    button_matrix[i + 6] = !digitalRead(sega_pin[i + 4]);
  }

  digitalWrite(SEL, HIGH);   // Set SEL to HIGH to read primary buttons
  delayMicroseconds(20);

  // Read UP, DOWN, LEFT, RIGHT, B, C
  for (byte i = 0; i < 6; i++) {
    button_matrix[i] = !digitalRead(sega_pin[i]);
  }

  digitalWrite(SEL, LOW);   // Set SEL to LOW to read additional buttons
  delayMicroseconds(20);
  digitalWrite(SEL, HIGH);   // Set SEL to LOW to read additional buttons
  delayMicroseconds(20);

  // Read Z, Y, X, MODE
  for (byte i = 0; i < 4; i++) {
    button_matrix[i + 8] = !digitalRead(sega_pin[i]);
  }

  digitalWrite(SEL, LOW);   // Set SEL to LOW to read additional buttons
  delayMicroseconds(20);
  digitalWrite(SEL, HIGH);   // Set SEL to LOW to read additional buttons
  // delayMicroseconds(20);
}

void PS2_press_key(byte key_number) {
  for (byte i = 0; i < 3; i++) {
    if (PS2_key_code[key_number][0][i] != 0x00) { // If keycode is not BLANK - Send key byte
      keyboard.write(PS2_key_code[key_number][0][i]); // send byte though ps2dev library
    }
  }
}

void PS2_release_key(byte key_number) {
  for (byte i = 0; i < 3 ; i++) {
    if (PS2_key_code[key_number][1][i] != 0x00) { // If keycode is not BLANK - Send key byte
      keyboard.write(PS2_key_code[key_number][1][i]); // send byte though ps2dev library
    }
  }
}

void setup() {
  // Serial.begin(19200); // ps2dev SPAMs debugging data though here. Very annying! Makes controller lag

  Keyboard.begin(); // starts 32u4 USB keyboard service
  USBCON |= (1 << OTGPADE); //enables VBUS pad to check if USB is connected

  //Prepare SEGA SEL pin
  pinMode(SEL, OUTPUT);
  digitalWrite(SEL, HIGH);

  // Set all SEGA pins to INPUT
  for (byte i = 0; i < 6; i++) {
    pinMode(sega_pin[i], INPUT_PULLUP);
  }
}

void loop() {
  unsigned char leds; // next lines need it dunno why
  keyboard.keyboard_handle(&leds); // ps2dev command. Allows to read HOST inputs and answer those

  if (millis() >= sega_timer) {
    sega_timer = millis() + 8;
    segaRead(); // read buttonpresses

    for (byte i = 0; i < 12; i++) {
      if (button_matrix[i] == 1 && prev_button_matrix[i] == 0) { // Gamepad button pressed
        if (USBSTA & (1 << VBUS)) Keyboard.press(USB_key_code[key_press[i]]);
        else PS2_press_key(key_press[i]);
      }

      if (button_matrix[i] == 0 && prev_button_matrix[i] == 1) { // Gamepad button relesed
        if (USBSTA & (1 << VBUS))Keyboard.release(USB_key_code[key_press[i]]);
        else PS2_release_key(key_press[i]);
      }
    }
    memcpy(prev_button_matrix, button_matrix, sizeof button_matrix); // Buffer the state of SEGA butons for further comparison (i.e. check if changed)
  }
}

The problem is that the resulting device just not reliable! Most of my PCs fail to accept 100% of keyboard bytes (either key-PRESS or RELEASE). I don’t know why:

• 2 inbred Compaq LTE 5000 laptops (like the one in the photo): sometimes commands (especially longer, 2-3 byte long ones) are not transferred successfully. I can’t complete a single 30-second racing lap without steering getting stuck.
• 2 identical IBM Thinkpads 560. Thing fails to initialize during bootup. The quirk of those laptops is that they are trying to initialize PS/2 devices by constantly spamming 0xF2 requests. After about average 2 minutes since booting – Arduino-as-PS/2-keyboard initializes and works as bad as with those Compaq LTE 5000 laptops above.
• 2 Desktop computers: one hot late-90s PC (Win 98, Pentium 800, VIA Apollo Pro 133 Chipset, Voodoo3 video) and 2011-2012 Win10 piece of crap. Keyboard initializes and works flawlessly 100% on both desktops. Though kinda pointless as these two PCs have USB ports.

Can you advice why is that?
Please advise why PS/2 commands most of the times succeed to be accepted but sometimes not?
What may be the reason of such non-100% reliability? Power? Static? My lame programming?

Feel free to glance at my original message of earlier iteration of the project here.

Please spare an advice, if you have any.

UPD. Fixed a small logical bug in the code above. Does not make it better with the old laptops. :frowning:

How is the PS/2 hardware wired?
Are you using transistors to drive the clock and data lines?
What value pullup resistors are you using on the clock and data lines?
How is the Arduino powered (from the PS/2 port)?

  • Hardware is wired directly to the pins;
  • No transistors utilized - Arduino innards do the heavy-lifting;
  • Internal pull-ups are used. I dunno their value (they pull up to + 4.94V);
  • Arduino is powered by PS/2 to VCC pin (I read + 4.96V with my voltmeter).

It's been a lo-o-ong project (about 6 months worth of episodic tinkering). I finally figured it out!!! :smiley: ;D :sunglasses:

DOS-era computers utilize PS/2 input differently from modern PCs. They lack processing power to monitor keyboard inputs constantly:

  • If HOST pulls DATA line down - it means HOST wants to send data. DEVICE has to orchestrate its receipt accordingly.
  • If HOST pulls CLOCK line down - it means HOST is wanking and DEVICE has to wait until it's done. If any data is sent during this private moment - it shall be ignored.

Modern PCs are mature enough not to resort to case #2 above. :wink: Therefore ps2dev.h and most of documentation online do not address it.

To elevate the issue you have to add the following line before sending PS/2 commands trough ps2dev.h:

while (digitalRead(YOUR_CLOCK_PIN_NUMBER) == 0) {}

(or you may edit ps2dev.cpp accordingly)

As for the issue with IBM Thinkpad 560, I found the following in the logs:

HOST sends 0xF0 (Set Scan Code Set)
ps2dev answers 0xFA (AKN)
HOST sends 0x00 (Asks DEVICE to remind the HOST which Scan Code is set now)
ps2dev answers 0xFA (AKN) (when it should have answered 0xFA and than 0x02)

This communication confuses Thinkpads and they fail to initialize. Perhaps due to a bug in ps2dev it initializes eventually. Dunno why - it shouldn't. I am writing my own PS/2 routines for better flexibility.

I am a happy DOS-gamer now! Now it's time to:

  • Add "typematic" routines,
  • Code PS/2 keyboard pass-though,
  • Add SNES connector and code routines for it,
  • Implement direct keyboard to game-pad key binding, and
  • Design a pretty PCB and a case for it!

BOY, THERE'S STUFF TO DO! :stuck_out_tongue_closed_eyes: :smiley: :grin:

Glad you figured out the problem, finding suitable hardware for connecting to old computer equipment used to be a pain until we managed to get rid of the old computers. :frowning: