Китайський перехідник PS/2 → USB: залипання клавіш, холод і чим його замінити
Стара 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-клавіатура на кожну клавішу шле два повідомлення: «натиснуто» і «відпущено». «Натиснуто» під час утримання повторюється, а «відпущено» передається один-єдиний раз. Якщо перехідник його загубить, 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 днів) | 20 | 4 |
| 23 вересня – 31 жовтня (39 днів) | 0 | 16 |
Чесно про межі: на клавіші Enter той самий підпис з'являється й далі. Enter інколи тримають навмисно, тож ці записи не доказ, але й не дозволяють сказати «збій зник повністю». Температуру в кімнаті ми не міряли — звʼязок із холодом поки гіпотеза, а не вимір.
Чому температура могла важити. Дріт тут ні до чого, а ось сама крапля — так. Без кварцу вона тактується вбудованим RC-генератором, а його частота залежить від температури. Програмне читання PS/2 в таких чіпах має малий запас по часу, і кілька відсотків зсуву досить, щоб точка зчитування біта виїхала за край. У холоді генератор зсувається в один бік, у теплі — в інший; якщо наш екземпляр на межі, тепло штовхає його за межу. Те саме стосується контактів у розʼємах: вони розширюються від тепла, і хиткий контакт стає хиткішим.
⚠ Тому влітку збої можуть повернутись. Що зробити до спеки:
- Поставити поруч кімнатний термометр із записом і тримати журнал залипань увімкненим — тоді звʼязок із теплом стане виміром.
- Допаяти підтяжки (по 10 кОм паралельно до наявних, разом ≈ 3 кОм) і конденсатори по живленню біля краплі: 0.1 мкФ керамічний і 22–47 мкФ електролітичний.
- Якщо не допоможе — замінити перехідник саморобним на мікроконтролері з нормальною прошивкою (нижче).
Огляд контролерів для саморобного перехідника
Перехіднику треба три речі: апаратний USB (щоб не емулювати його програмно), входи, що витримують 5 В лінії PS/2 (або перетворювач рівнів), і готова прошивка, яку перевірили тисячі людей.
| Контролер / плата | USB | 5 В на входах | Готова прошивка | Висновок |
|---|---|---|---|---|
| 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_usb | ATmega32U4 (Pro Micro, Teensy 2.0) | PS/2, AT і XT, набори кодів 1–3, світлодіоди Caps/Num, перепризначення клавіш; вважається еталоном |
| Soarer’s Converter | ATmega32U4 (Teensy 2.0, Pro Micro) | PS/2, AT, XT і термінальні клавіатури, гнучке перепризначення через конфіг-файл |
| ps2x2pico | RP2040 (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 | Перша версія: симптом і журнал залипань, причини, холодний період, огляд контролерів і прошивок, приклад прошивки. |