Workshop: repairs and experiments

A cheap Chinese PS/2 to USB adapter: sticky keys, the cold and what to replace it with

Published 06.11.2026 · Updated 07.11.2026

An old PS/2 keyboard, a cheap Chinese USB adapter — and every now and then a key “sticks”: a letter pours out in a row as if it were held down. Here is what exactly fails in such an adapter, what we checked, why the glitches on letters disappeared when the weather turned cold (and why they may come back in summer), plus an overview of microcontrollers and ready-made firmware for building an adapter that does not do this.

What is inside a cheap adapter

PS/2 → USB adapters come in two kinds, and they must not be confused:

KindHow it is builtWhich keyboard it works with
passivejust wires: the USB plug rewired to mini-DINonly a “combo” keyboard (USB/PS2 on the label): it can speak USB by itself
activea microcontroller reads the PS/2 protocol and pretends to be a USB keyboardany PS/2 keyboard

Ours is active: a black epoxy blob on the board. That is not glue but the chip itself: the die is glued to the board, bonded with thin wires and potted (so-called COB, chip-on-board). It has no marking, and Windows sees the device as VID_13BA + PID_0018 — a composite: keyboard plus mouse.

Our PS/2 to USB adapter
Our adapter: a USB plug, a small box with the board and two PS/2 sockets — purple for the keyboard, green for the mouse.
The PS2-04H board, the side with pin labels
The PS2-04H board, back side. On the USB side — pads VDD, DN, DP, GND; on the PS/2 side — KEYBOARD and MOUSE, each with GND, DATA, CLK, VDD.
Front of the board: the chip under a black blob of potting compound
Front side: the chip sits under a black blob of potting compound (COB). There is no crystal on the board, so the chip runs from its built-in oscillator.

🔑 The main consequence: the firmware of such an adapter cannot be fixed. It is burned into the die at the factory, and there are no programming pins. You can only remove the conditions under which it fails, or replace the adapter.

The symptom: the key is released, but it keeps typing

A PS/2 keyboard sends two messages for every key: “pressed” and “released”. “Pressed” repeats while the key is held, but “released” is sent exactly once. If the adapter loses it, Windows thinks the key is still down and starts auto-repeat: after a half-second pause the letter pours out in a row.

To avoid guessing, we kept a log: a trap program recorded only anomalies (long runs of repeats), not normal typing. From 11 to 22 September it caught 20 sticky keys on letters, all with the same signature:

SignWhat we sawWhat it means
repeats per runexactly 16, every timenot a random duration but a fixed timer
duration1000–1032 msthe release does arrive — about a second later
keys14 different ones: J, L, G, D, B, H, R, V, T, M, W, Z…not one bad key, but the link between keyboard and USB
pairsJ → L within 1 s, D → B within 1 s, H → R within 1 sduring the glitch window the release of ANY key is lost

Nobody holds a letter down on purpose, which is why letters became the proof. The same signature on different keys means the mechanical contact is not to blame: keys do not all “let go” exactly one second later. The closest explanation is that the adapter loses the release, and a second later its own timer “releases” the key by itself.

Why the release gets lost

PS/2 is two lines, CLK and DATA. A zero is pulled down by the device, while a one is made only by a pull-up resistor to +5 V. The weaker the pull-up and the longer the cable, the slower the line rises to one and the easier noise knocks it over. Cheap adapters save money right here:

Weak spotWhat happens
weak pull-upsa slow signal edge: the chip samples the bit at the wrong moment and the frame breaks
clock without a crystalthe built-in RC oscillator drifts by a few percent with temperature and voltage, and the timing margin is thin already
no supply capacitorsa 5 V dip while typing — a receiver glitch
no checking and no retryPS/2 does not ask again: a broken “released” frame is never repeated

We measured the pull-ups in our adapter with a multimeter (adapter unplugged, probes on the socket): CLK — 4.4 kΩ, DATA — 4.38 kΩ. Not a disaster, but no margin either.

What we checked — and what did not help

What we didResult
removed the PS/2 extension cable (the cable was 3–5 m)sticky keys remained
disabled USB power saving for the adapter and hubs, and selective suspendsticky keys remained
started an anomaly logfound the “16 repeats, ~1 s” signature on letters — 20 times in 12 days
measured the pull-ups4.4 / 4.38 kΩ; the plan is to add 10 kΩ in parallel to each (≈ 3 kΩ total)

We ruled out heating of the wire itself by calculation: the keyboard draws about 100 mA, which on two metres of thin cable is milliwatts. The wire does not heat up, and its resistance plays no part in the failure mechanism at all.

It got colder — and the glitches on letters stopped

The log recorded the last sticky letter on 22 September. From 23 September to 31 October — not one, although we changed neither the adapter, nor the keyboard, nor any settings. This coincided with the cold: the room got cooler, and the adapter “fixed itself”.

PeriodSticky letters (16 repeats)Same signature on Enter
11–22 September (12 days)204
23 September – 31 October (39 days)016

To be honest about the limits: the same signature still appears on the Enter key. Enter is sometimes held on purpose, so these records are not proof, but they do not let us say “the glitch is completely gone” either. We did not measure the room temperature — the link with the cold is a hypothesis for now, not a measurement.

Why temperature could matter. The wire has nothing to do with it, but the blob itself does. Without a crystal it is clocked by a built-in RC oscillator whose frequency depends on temperature. Software PS/2 reading in such chips has a small timing margin, and a few percent of drift is enough to push the bit sampling point over the edge. In the cold the oscillator drifts one way, in the warmth the other; if our unit is on the edge, warmth pushes it over. The same goes for the connector contacts: they expand with heat, and a shaky contact gets shakier.

⚠ So the glitches may come back in summer. What to do before the heat:

  1. Put a logging room thermometer next to it and keep the sticky-key log running — then the link with heat becomes a measurement.
  2. Add pull-ups (10 kΩ in parallel to the existing ones, ≈ 3 kΩ total) and supply capacitors next to the blob: 0.1 µF ceramic and 22–47 µF electrolytic.
  3. If that does not help, replace the adapter with a home-made one on a microcontroller with proper firmware (below).

Microcontrollers for a home-made adapter

An adapter needs three things: hardware USB (so USB is not emulated in software), inputs that tolerate the 5 V PS/2 lines (or a level shifter), and ready firmware tested by thousands of people.

Controller / boardUSB5 V on inputsReady firmwareVerdict
ATmega32U4 (Arduino Pro Micro 5 V, Leonardo, Teensy 2.0)hardwareyes, runs from 5 VTMK ibmpc_usb, Soarer’s Converterthe best choice for a keyboard
RP2040 (Raspberry Pi Pico)hardwareno, 3.3 V only — needs a level shifterps2x2pico (keyboard and mouse)good, especially if you need a mouse too
STM32F103 (Blue Pill)hardwaresome pins are 5 V tolerantwe cannot name a well-known ready converter firmwarepossible, but you will write it yourself
ESP32-S2 / S3hardwareno, 3.3 V onlywe cannot name a well-known ready oneoverkill for this job
ATtiny85 (Digispark) with V-USBsoftware (emulated)yeshobby projects existnot recommended: USB on the edge of timing — one more source of glitches
ATmega328P (Arduino Uno, Nano)none (only a USB-UART bridge)yes—not suitable: it will not become a PC keyboard
ESP32 (classic), ESP8266noneno—not suitable
the blob in a Chinese adapterhardwareyesfactory, cannot be reflashedworks until it hits the edge

⚠ The Pro Micro comes in two versions: 5 V / 16 MHz and 3.3 V / 8 MHz. For PS/2 take only the 5 V one: the keyboard is powered from 5 V, and its lines are 5 V too.

⚠ The RP2040 does not tolerate 5 V on its inputs. The CLK and DATA lines go through a bidirectional level shifter (a ready-made board with BSS138 MOSFETs), not directly.

Ready-made firmware

FirmwareBoardWhat it does
TMK, ibmpc_usb converterATmega32U4 (Pro Micro, Teensy 2.0)PS/2, AT and XT, scan code sets 1–3, Caps/Num LEDs, key remapping; considered the reference
Soarer’s ConverterATmega32U4 (Teensy 2.0, Pro Micro)PS/2, AT, XT and terminal keyboards, flexible remapping through a config file
ps2x2picoRP2040 (Pico)PS/2 keyboard and mouse at the same time, reading via PIO

How to flash:

# Pro Micro (Caterina bootloader): short RST to GND twice quickly —
# the board stays in bootloader mode for 8 seconds, a new COM port appears
avrdude -p atmega32u4 -c avr109 -P COM5 -U flash:w:firmware.hex

# Raspberry Pi Pico: hold BOOTSEL while plugging into USB —
# an RPI-RP2 drive appears, copy the .uf2 file onto it

Example firmware: how not to leave a key stuck

To show how good firmware differs from the blob, here is a learning example for the Pro Micro (keyboard only, scan code set 2, no LEDs). What matters in it is not the key table but the behaviour on error:

  • every frame is checked: start bit, parity, stop bit; a pause over 2 ms in the middle of a frame means lost sync;
  • if a frame is broken, it may have been exactly a “released”, so the firmware releases all keys. The only thing worse than nothing is a key that types by itself;
  • if the keyboard restarted (self-test code) or overflowed its buffer — release everything as well.

Wiring: PS/2 socket pin 1 (DATA) to pin 4, pin 5 (CLK) to pin 3, pin 4 to VCC, pin 3 to GND; a 4.7 kΩ resistor from CLK and from DATA to +5 V.

⚠ We have not compiled this example or tested it on hardware. For everyday use take TMK or Soarer’s Converter: they have everything this one lacks (LEDs, commands to the keyboard, retrying broken frames).

ps2_to_usb_promicro.ino · 7274 B

Show the code (227 lines)
// PS/2 keyboard -> USB HID on Arduino Pro Micro (ATmega32U4, 5 V / 16 MHz).
// Educational example: keyboard only, scan code set 2, no LEDs, no host-to-keyboard commands.
// For daily use take a complete converter firmware (TMK ibmpc_usb, Soarer's Converter).
//
// Wiring (PS/2 socket pin -> Pro Micro):
//   1 DATA -> pin 4,  5 CLK -> pin 3 (INT0),  4 +5V -> VCC,  3 GND -> GND
//   4.7 kOhm pull-up from DATA to +5 V and from CLK to +5 V.
//
// The main idea vs. a cheap adapter: if a frame is damaged, the "key released" code may be lost,
// so on any receive error we release ALL keys instead of leaving one key stuck.

#include <Keyboard.h>

const uint8_t PIN_CLK = 3;
const uint8_t PIN_DATA = 4;

// Ring buffer filled in the interrupt, emptied in loop().
volatile uint8_t rx[32];
volatile uint8_t rxHead = 0;
volatile uint8_t rxTail = 0;
volatile bool rxError = false;

void onClockFall() {
  static uint8_t bitNo = 0;
  static uint8_t value = 0;
  static uint8_t ones = 0;
  static unsigned long lastUs = 0;

  unsigned long now = micros();
  // A PS/2 bit lasts 60-100 us. A gap over 2 ms means we lost sync: start a new frame.
  if (bitNo != 0 && now - lastUs > 2000) {
    bitNo = 0;
    rxError = true;
  }
  lastUs = now;

  uint8_t d = digitalRead(PIN_DATA);
  if (bitNo == 0) {                 // start bit must be 0
    if (d != 0) {
      return;
    }
    value = 0;
    ones = 0;
  } else if (bitNo <= 8) {          // 8 data bits, LSB first
    if (d) {
      value |= (uint8_t)(1 << (bitNo - 1));
      ones++;
    }
  } else if (bitNo == 9) {          // odd parity
    if (((ones + d) & 1) == 0) {
      rxError = true;
    }
  } else {                          // stop bit must be 1
    if (d == 1) {
      uint8_t next = (rxHead + 1) & 31;
      if (next != rxTail) {
        rx[rxHead] = value;
        rxHead = next;
      } else {
        rxError = true;             // buffer overflow
      }
    } else {
      rxError = true;
    }
    bitNo = 0;
    return;
  }
  bitNo++;
}

// Set 2 scan code -> value for Keyboard.press(): 0x80..0x87 modifiers, 136 + HID usage for other keys.
uint8_t mapKey(uint8_t code, bool ext) {
  if (ext) {
    switch (code) {
      case 0x14: return KEY_RIGHT_CTRL;
      case 0x11: return KEY_RIGHT_ALT;
      case 0x1F: return KEY_LEFT_GUI;
      case 0x27: return KEY_RIGHT_GUI;
      case 0x75: return 136 + 0x52;  // Up
      case 0x72: return 136 + 0x51;  // Down
      case 0x6B: return 136 + 0x50;  // Left
      case 0x74: return 136 + 0x4F;  // Right
      case 0x70: return 136 + 0x49;  // Insert
      case 0x71: return 136 + 0x4C;  // Delete
      case 0x6C: return 136 + 0x4A;  // Home
      case 0x69: return 136 + 0x4D;  // End
      case 0x7D: return 136 + 0x4B;  // Page Up
      case 0x7A: return 136 + 0x4E;  // Page Down
      case 0x5A: return 136 + 0x58;  // Keypad Enter
      case 0x4A: return 136 + 0x54;  // Keypad /
      default: return 0;             // E0 12 / E0 59 (fake shifts of Print Screen) and the rest are ignored
    }
  }
  switch (code) {
    case 0x1C: return 136 + 0x04;  // A
    case 0x32: return 136 + 0x05;  // B
    case 0x21: return 136 + 0x06;  // C
    case 0x23: return 136 + 0x07;  // D
    case 0x24: return 136 + 0x08;  // E
    case 0x2B: return 136 + 0x09;  // F
    case 0x34: return 136 + 0x0A;  // G
    case 0x33: return 136 + 0x0B;  // H
    case 0x43: return 136 + 0x0C;  // I
    case 0x3B: return 136 + 0x0D;  // J
    case 0x42: return 136 + 0x0E;  // K
    case 0x4B: return 136 + 0x0F;  // L
    case 0x3A: return 136 + 0x10;  // M
    case 0x31: return 136 + 0x11;  // N
    case 0x44: return 136 + 0x12;  // O
    case 0x4D: return 136 + 0x13;  // P
    case 0x15: return 136 + 0x14;  // Q
    case 0x2D: return 136 + 0x15;  // R
    case 0x1B: return 136 + 0x16;  // S
    case 0x2C: return 136 + 0x17;  // T
    case 0x3C: return 136 + 0x18;  // U
    case 0x2A: return 136 + 0x19;  // V
    case 0x1D: return 136 + 0x1A;  // W
    case 0x22: return 136 + 0x1B;  // X
    case 0x35: return 136 + 0x1C;  // Y
    case 0x1A: return 136 + 0x1D;  // Z
    case 0x16: return 136 + 0x1E;  // 1
    case 0x1E: return 136 + 0x1F;  // 2
    case 0x26: return 136 + 0x20;  // 3
    case 0x25: return 136 + 0x21;  // 4
    case 0x2E: return 136 + 0x22;  // 5
    case 0x36: return 136 + 0x23;  // 6
    case 0x3D: return 136 + 0x24;  // 7
    case 0x3E: return 136 + 0x25;  // 8
    case 0x46: return 136 + 0x26;  // 9
    case 0x45: return 136 + 0x27;  // 0
    case 0x5A: return 136 + 0x28;  // Enter
    case 0x76: return 136 + 0x29;  // Esc
    case 0x66: return 136 + 0x2A;  // Backspace
    case 0x0D: return 136 + 0x2B;  // Tab
    case 0x29: return 136 + 0x2C;  // Space
    case 0x4E: return 136 + 0x2D;  // -
    case 0x55: return 136 + 0x2E;  // =
    case 0x54: return 136 + 0x2F;  // [
    case 0x5B: return 136 + 0x30;  // ]
    case 0x5D: return 136 + 0x31;  // backslash
    case 0x4C: return 136 + 0x33;  // ;
    case 0x52: return 136 + 0x34;  // '
    case 0x0E: return 136 + 0x35;  // `
    case 0x41: return 136 + 0x36;  // ,
    case 0x49: return 136 + 0x37;  // .
    case 0x4A: return 136 + 0x38;  // /
    case 0x58: return 136 + 0x39;  // Caps Lock
    case 0x05: return 136 + 0x3A;  // F1
    case 0x06: return 136 + 0x3B;  // F2
    case 0x04: return 136 + 0x3C;  // F3
    case 0x0C: return 136 + 0x3D;  // F4
    case 0x03: return 136 + 0x3E;  // F5
    case 0x0B: return 136 + 0x3F;  // F6
    case 0x83: return 136 + 0x40;  // F7
    case 0x0A: return 136 + 0x41;  // F8
    case 0x01: return 136 + 0x42;  // F9
    case 0x09: return 136 + 0x43;  // F10
    case 0x78: return 136 + 0x44;  // F11
    case 0x07: return 136 + 0x45;  // F12
    case 0x12: return KEY_LEFT_SHIFT;
    case 0x59: return KEY_RIGHT_SHIFT;
    case 0x14: return KEY_LEFT_CTRL;
    case 0x11: return KEY_LEFT_ALT;
    default: return 0;
  }
}

void setup() {
  pinMode(PIN_CLK, INPUT);   // external 4.7 kOhm pull-ups do the job
  pinMode(PIN_DATA, INPUT);
  Keyboard.begin();
  attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClockFall, FALLING);
}

void loop() {
  static bool ext = false;
  static bool brk = false;
  static uint8_t skip = 0;

  if (rxError) {
    // A damaged or lost frame may have been a "release" code. Releasing everything is safer
    // than a key that repeats until the next press.
    rxError = false;
    Keyboard.releaseAll();
    ext = false;
    brk = false;
  }

  while (rxTail != rxHead) {
    uint8_t c = rx[rxTail];
    rxTail = (rxTail + 1) & 31;

    if (skip) {                    // rest of the Pause sequence
      skip--;
      continue;
    }
    if (c == 0xE1) {               // Pause: E1 14 77 E1 F0 14 F0 77
      skip = 7;
      continue;
    }
    if (c == 0xAA || c == 0x00 || c == 0xFF) {
      // AA: keyboard self-test passed (it was reconnected or reset); 00/FF: keyboard buffer overrun.
      Keyboard.releaseAll();
      ext = false;
      brk = false;
      continue;
    }
    if (c == 0xE0) {
      ext = true;
      continue;
    }
    if (c == 0xF0) {
      brk = true;
      continue;
    }
    uint8_t k = mapKey(c, ext);
    if (k) {
      if (brk) {
        Keyboard.release(k);
      } else {
        Keyboard.press(k);
      }
    }
    ext = false;
    brk = false;
  }
}

What to choose

SituationWhat to do
a “combo” keyboard (USB/PS2 on the label)a passive adapter — no chips, nothing to fail
the adapter glitches rarelyfirst the pull-ups and capacitors (soldering iron, 15 minutes), with a log before and after
you want it reliable for goodPro Micro 5 V + TMK ibmpc_usb
you also need a PS/2 mouseRaspberry Pi Pico + ps2x2pico, with a level shifter
you just want to check quickly whether the adapter is to blamebuy another active adapter: the firmware in blobs differs

Summary

QuestionAnswer
what failsthe adapter loses the “released” code; after ~1 s its timer releases the key by itself
is the keyboard to blameno: the same signature on 14 different keys
is the cable to blamewe removed the extension — it did not help; wire heating is ruled out
what changedfrom 23 September (it got colder) — not a single sticky letter until the end of October
what about summerit may come back; before the heat — pull-ups, capacitors, a thermometer nearby
the permanent fixan ATmega32U4 adapter with TMK firmware

Article changelog

DateWhat changed
2026-11-07Photos of the adapter and both sides of its board.
2026-11-02First version: symptom and sticky-key log, causes, the cold period, controllers and firmware overview, example firmware.