Майстерня: ремонт і досліди

Китайський перехідник PS/2 → USB: залипання клавіш, холод і чим його замінити

Опубліковано 06.11.2026 · Оновлено 07.11.2026

Стара PS/2-клавіатура, дешевий китайський перехідник на USB — і час від часу клавіша «залипає»: літера сиплеться рядком, ніби її тримають. Тут — що саме ламається в такому перехіднику, що ми перевірили, чому з похолоданням збої на літерах зникли (і чому влітку вони можуть повернутись), а також огляд мікроконтролерів і готових прошивок, щоб зібрати перехідник, який так не робить.

Що всередині дешевого перехідника

Перехідники PS/2 → USB бувають двох видів, і плутати їх не можна:

ВидЯк влаштованийЗ якою клавіатурою працює
пасивнийлише дроти: розʼєм USB перепаяний на mini-DINтільки з «комбо»-клавіатурою (на дні напис USB/PS2): вона сама вміє говорити по USB
активниймікроконтролер читає протокол PS/2 і прикидається USB-клавіатуроюз будь-якою PS/2-клавіатурою

У нашому — активний: чорна крапля епоксидки на платі. Це не клей, а сама мікросхема: кристал приклеєний до плати, розварений тонкими дротиками й залитий компаундом (так звана технологія COB). Маркування на ній немає, а Windows бачить пристрій як VID_13BA + PID_0018 — складений: клавіатура й мишка.

Наш перехідник PS/2 → USB
Наш перехідник: штекер USB, коробочка з платою і два гнізда PS/2 — фіолетове для клавіатури, зелене для мишки.
Плата PS2-04H, бік із підписами контактів
Плата PS2-04H, зворотний бік. З боку USB — контакти VDD, DN, DP, GND; з боку PS/2 — KEYBOARD і MOUSE, у кожного GND, DATA, CLK, VDD.
Лицьовий бік плати: мікросхема під чорною краплею компаунду
Лицьовий бік: мікросхема — під чорною краплею компаунду (COB). Кварцу на платі немає, тож чіп тактується від вбудованого генератора.

🔑 Головний наслідок: прошивку такого перехідника не виправити. Вона зашита в кристал на заводі, виводів для програматора немає. Можна лише прибрати умови, за яких вона помиляється, або замінити перехідник.

Симптом: клавішу відпустили, а вона друкує далі

PS/2-клавіатура на кожну клавішу шле два повідомлення: «натиснуто» і «відпущено». «Натиснуто» під час утримання повторюється, а «відпущено» передається один-єдиний раз. Якщо перехідник його загубить, Windows вважає клавішу затиснутою й вмикає автоповтор: після паузи пів секунди літера сиплеться рядком.

Щоб не гадати, ми вели журнал: програма-пастка записувала лише аномалії (довгі серії повторів), без звичайного набору. За 11–22 вересня вона зловила 20 залипань на літерах, і всі з однаковим підписом:

ОзнакаЩо бачилиЩо це означає
повторів у серіїрівно 16, кожного разуне випадкова тривалість, а сталий таймер
тривалість1000–1032 мсвідпускання таки приходить — приблизно через секунду
клавіші14 різних: J, L, G, D, B, H, R, V, T, M, W, Z…винна не одна кнопка, а ланка між клавіатурою і USB
париJ → L за 1 с, D → B за 1 с, H → R за 1 су вікні збою губиться відпускання БУДЬ-ЯКОЇ клавіші

Літеру ніхто не тримає затиснутою навмисно, тому саме літери й стали доказом. Однаковий підпис на різних кнопках означає, що механічний контакт не винен: кнопки не «відпускаються» всі рівно через секунду. Найближче пояснення — перехідник губить відпускання, а через секунду спрацьовує його власний таймер і «відпускає» клавішу сам.

Чому губиться відпускання

PS/2 — дві лінії, CLK і DATA. Нуль на лінії тягне пристрій, а одиницю робить лише резистор підтяжки до +5 В. Що слабша підтяжка й довший кабель, то повільніше лінія піднімається до одиниці і то легше її збиває завада. Дешеві перехідники економлять саме тут:

Слабке місцеЩо відбувається
слабкі підтяжкифронт сигналу повільний: мікросхема читає біт не в тій точці, і кадр бʼється
генератор такту без кварцувбудований RC-генератор «пливе» від температури й напруги на кілька відсотків, а запас по часу і так тонкий
немає конденсаторів по живленнюпросадка 5 В під час набору — збій приймача
немає перевірки й повторуPS/2 не перепитує загублене: битий кадр «відпущено» не повториться ніколи

У нашому перехіднику ми поміряли підтяжки мультиметром (перехідник вийнятий, щупи на гніздо): CLK — 4.4 кОм, DATA — 4.38 кОм. Це не катастрофа, але й без запасу.

Що ми перевірили — і що не допомогло

Що зробилиРезультат
прибрали PS/2-подовжувач (кабель був 3–5 м)залипання лишились
вимкнули присипляння USB для перехідника й хабів, selective suspendзалипання лишились
почали журнал аномалійзнайшли підпис «16 повторів, ~1 с» на літерах — 20 разів за 12 днів
поміряли підтяжки4.4 / 4.38 кОм; план — додати паралельно по 10 кОм (разом ≈ 3 кОм)

Нагрів самого дроту ми виключили розрахунком: струм клавіатури — близько 100 мА, на двох метрах тонкого кабелю це мілівати. Дріт не гріється, а його опір у механізмі збою не бере участі взагалі.

Похолодало — і збої на літерах зникли

Останнє залипання на літері журнал записав 22 вересня. З 23 вересня до 31 жовтня — жодного, хоча перехідник, клавіатуру й налаштування ми не міняли. Збіглося це з похолоданням: стало прохолодніше в кімнаті, і перехідник «сам виправився».

ПеріодЗалипань на літерах (16 повторів)Той самий підпис на Enter
11–22 вересня (12 днів)204
23 вересня – 31 жовтня (39 днів)016

Чесно про межі: на клавіші Enter той самий підпис з'являється й далі. Enter інколи тримають навмисно, тож ці записи не доказ, але й не дозволяють сказати «збій зник повністю». Температуру в кімнаті ми не міряли — звʼязок із холодом поки гіпотеза, а не вимір.

Чому температура могла важити. Дріт тут ні до чого, а ось сама крапля — так. Без кварцу вона тактується вбудованим RC-генератором, а його частота залежить від температури. Програмне читання PS/2 в таких чіпах має малий запас по часу, і кілька відсотків зсуву досить, щоб точка зчитування біта виїхала за край. У холоді генератор зсувається в один бік, у теплі — в інший; якщо наш екземпляр на межі, тепло штовхає його за межу. Те саме стосується контактів у розʼємах: вони розширюються від тепла, і хиткий контакт стає хиткішим.

⚠ Тому влітку збої можуть повернутись. Що зробити до спеки:

  1. Поставити поруч кімнатний термометр із записом і тримати журнал залипань увімкненим — тоді звʼязок із теплом стане виміром.
  2. Допаяти підтяжки (по 10 кОм паралельно до наявних, разом ≈ 3 кОм) і конденсатори по живленню біля краплі: 0.1 мкФ керамічний і 22–47 мкФ електролітичний.
  3. Якщо не допоможе — замінити перехідник саморобним на мікроконтролері з нормальною прошивкою (нижче).

Огляд контролерів для саморобного перехідника

Перехіднику треба три речі: апаратний USB (щоб не емулювати його програмно), входи, що витримують 5 В лінії PS/2 (або перетворювач рівнів), і готова прошивка, яку перевірили тисячі людей.

Контролер / платаUSB5 В на входахГотова прошивкаВисновок
ATmega32U4 (Arduino Pro Micro 5 В, Leonardo, Teensy 2.0)апаратнийтак, живиться від 5 ВTMK ibmpc_usb, Soarer’s Converterнайкращий вибір для клавіатури
RP2040 (Raspberry Pi Pico)апаратнийні, лише 3.3 В — потрібен перетворювач рівнівps2x2pico (клавіатура й мишка)добре, особливо якщо потрібна й мишка
STM32F103 (Blue Pill)апаратнийчастина виводів витримує 5 Ввідомої готової прошивки-перехідника не назвемоможна, але доведеться писати самому
ESP32-S2 / S3апаратнийні, лише 3.3 Ввідомої готової не назвемонадлишково для такої задачі
ATtiny85 (Digispark) з V-USBпрограмний (емуляція)такє аматорськіне радимо: USB «на межі» таймінгів — ще одне джерело збоїв
ATmega328P (Arduino Uno, Nano)немає (лише перетворювач USB-UART)так—не підходить: клавіатурою для ПК не стане
ESP32 (класичний), ESP8266немаєні—не підходить
крапля в китайському перехідникуапаратнийтакзаводська, не перепрошитипрацює, поки не потрапить на межу

⚠ Pro Micro буває у двох виконаннях: 5 В / 16 МГц і 3.3 В / 8 МГц. Для PS/2 брати лише 5 В: клавіатура живиться від 5 В, і її лінії теж 5-вольтові.

⚠ RP2040 не витримує 5 В на входах. Лінії CLK і DATA підключають через двонаправлений перетворювач рівнів (готова платка на польових транзисторах BSS138), а не напряму.

Готові прошивки

ПрошивкаПлатаЩо вміє
TMK, конвертер ibmpc_usbATmega32U4 (Pro Micro, Teensy 2.0)PS/2, AT і XT, набори кодів 1–3, світлодіоди Caps/Num, перепризначення клавіш; вважається еталоном
Soarer’s ConverterATmega32U4 (Teensy 2.0, Pro Micro)PS/2, AT, XT і термінальні клавіатури, гнучке перепризначення через конфіг-файл
ps2x2picoRP2040 (Pico)клавіатура й мишка PS/2 одночасно, читання через PIO

Як залити:

# Pro Micro (завантажувач Caterina): двічі швидко замкнути RST на GND —
# 8 секунд плата в режимі прошивки, зʼявляється новий COM-порт
avrdude -p atmega32u4 -c avr109 -P COM5 -U flash:w:firmware.hex

# Raspberry Pi Pico: тримаючи BOOTSEL, увімкнути в USB —
# зʼявиться диск RPI-RP2, на нього скопіювати файл .uf2

Приклад прошивки: як не лишити клавішу затиснутою

Щоб було видно, чим добра прошивка відрізняється від краплі, ось навчальний приклад для Pro Micro (лише клавіатура, набір кодів 2, без світлодіодів). Головне в ньому — не таблиця клавіш, а поведінка при помилці:

  • кожен кадр перевіряється: стартовий біт, парність, стоповий біт; пауза понад 2 мс посеред кадру — втрата синхронізації;
  • якщо кадр битий, він міг бути саме «відпущено», тож прошивка відпускає всі клавіші. Гірше, ніж нічого, — лише клавіша, що друкує сама;
  • якщо клавіатура перезапустилась (код самоперевірки) або переповнила буфер — теж відпустити все.

Підключення: контакт 1 гнізда PS/2 (DATA) — на вивід 4, контакт 5 (CLK) — на вивід 3, контакт 4 — на VCC, контакт 3 — на GND; на CLK і DATA — по резистору 4.7 кОм до +5 В.

⚠ Приклад ми не компілювали й не перевіряли на залізі. Для щоденної роботи беріть TMK або Soarer’s Converter: там є все, чого тут немає (світлодіоди, команди клавіатурі, повтор битих кадрів).

ps2_to_usb_promicro.ino · 7274 B

Показати код (227 рядків)
// 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;
  }
}

Що вибрати

СитуаціяЩо робити
клавіатура «комбо» (на дні USB/PS2)пасивний перехідник — без мікросхем, ламатись нічому
перехідник збоїть зрідкаспершу підтяжки й конденсатори (паяльник, 15 хвилин), журнал до і після
хочеться надійно і назавждиPro Micro 5 В + TMK ibmpc_usb
потрібна ще й PS/2-мишкаRaspberry Pi Pico + ps2x2pico, з перетворювачем рівнів
лише швидко перевірити, чи винен перехідниккупити інший активний перехідник: прошивки в краплях різні

Підсумок

ПитанняВідповідь
що ламаєтьсяперехідник губить код «відпущено»; через ~1 с його таймер відпускає клавішу сам
чи винна клавіатурані: однаковий підпис на 14 різних кнопках
чи винен кабельподовжувач прибрали — не допомогло; нагрів дроту виключено
що змінилосьз 23 вересня (похолодало) — жодного залипання на літерах до кінця жовтня
що буде вліткуможливо, повернеться; до спеки — підтяжки, конденсатори, термометр поруч
рішення назавждиперехідник на ATmega32U4 з прошивкою TMK

Журнал змін статті

ДатаЩо змінилось
07.11.2026Фото перехідника й обох боків його плати.
02.11.2026Перша версія: симптом і журнал залипань, причини, холодний період, огляд контролерів і прошивок, приклад прошивки.