PS2 to USB

HardwareDIYKVM

Some KVMs and USB hosts want a plain HID keyboard on the wire, not a composite gadget that also exposes USB serial. This board is a small PS/2 keyboard to USB bridge built around a SparkFun Pro Micro (ATmega32U4). It reads the PS/2 clock and data lines, translates scancodes with the PS2KeyAdvanced library, and sends Boot Keyboard and consumer (media) reports using HID-Project. The default firmware build matches that “basic keyboard” expectation, which is what I needed for ATEN KVMs while still using a vintage PS/2 keyboard.

What you get

  • Standard keys, modifiers, arrows, editing block, keypad, and F1–F24 mapped where the PS/2 stack provides codes.
  • Media keys (volume, transport, browser, power/sleep, and similar) via HID consumer usage.
  • Caps / Num / Scroll lock: the adapter follows the host LED state and forwards lock state to the PS/2 side so the keyboard’s lock LEDs stay coherent.
  • Letters and case: Caps Lock and Shift are combined so uppercase/lowercase matches what the host expects.
  • Safe mode: optional strap from D2 to GND at boot keeps HID off (handy for recovery or picky enumeration); see Firmware below.

Photos, PCB, and schematic

PCB layout render for the PS2 to USB board
Board artwork (schematic PNG and KiCad sources below).
Schematic: Pro Micro, PS/2 connector, capacitors, and switches
Schematic export — same netlist as PS2ToUSB.kicad_sch.

Bill of materials

QtyItemNotes
1SparkFun Pro MicroSparkFun Pro Micro; USB-C or Micro-B depending on the board you buy.
1Mini-DIN-6 female, PCB mountKeyboard PS/2 socket; example search: “Mini DIN 6 female socket”.
210µF capacitor, 0805Unpolarized ceramic per board (example LCSC listing).
46mm tactile switches (optional)THT footprint on PCB (example LCSC listing).

The KiCad project also includes power nets, shield, and Pro Micro pinout as in the schematic. If you substitute another ATmega32U4 board, re-map D3/D4/D2 in firmware to match your wiring.

PS/2 connector pinout

Pin numbering matches the Mini-DIN-6 symbol in the KiCad library used for this project (data and clock on pins 1 and 5, +5V on 4, ground on 3).

Mini-DIN-6 (keyboard PS/2), schematic pin numbers

Annotated PS/2 mini-DIN pinout photo for this PCB
Pin Description
1 Data (to ATmega PS/2 data, firmware DATAPIN 4)
2 Not used on this design (NC in symbol)
3 Ground
4 +5V (from Pro Micro / USB rail per schematic)
5 Clock (to ATmega PS/2 clock, firmware IRQPIN 3)
6 Not used (NC)
Shell Shield / chassis tie per PCB

Firmware pin assignment (Arduino pin numbers on the Pro Micro):

SignalArduino pinRole
PS/2 data4DATAPIN
PS/2 clock3IRQPIN (interrupt-capable pin for the library)
Safe mode (to GND)2SAFEPIN — hold low at boot to keep HID off

Ordering the PCB (gerbers)

Production gerbers are bundled for a fab such as JLCPCB or PCBWay:

Download ps2usb-gerbers.zip

Typical flow: upload the zip, choose 2-layer specs matching the board (1.6mm FR4, HASL or ENIG, your color), and confirm copper layers and outline in the fab’s preview. Order Stencil: no unless you plan reflow for the handful of SMD parts.

Assembly

  1. Solder SMD first — the two 10µF 0805 capacitors are easier before the tall THT parts.
  2. Mini-DIN-6 — support the connector while soldering; ensure pins match the silkscreen / schematic orientation so clock and data are not swapped.
  3. Optional tact switches — populate if you want panel controls; one switch position is intended to strap D2 to ground for safe mode (see firmware). If you skip switches, you can use a jumper wire for recovery the same way.
  4. Pro Micro — last, so you can still reach the underside if needed. Match USB connector direction to the board outline so the cable exits the intended edge.
  5. Inspect — continuity-check GND and +5V to the PS/2 socket before first insert of the Pro Micro, and verify no shorts between adjacent DIN pins.

Firmware

The sketch is firmware/PS22USB.ino. It uses:

  • PS2KeyAdvanced — PS/2 decode and lock handling.
  • HID-ProjectBootKeyboard and Consumer HID interfaces for broad host compatibility.

USB: HID-only vs CDC + HID

On ATmega32U4, USB interfaces are fixed at compile time. The Makefile passes -DCDC_DISABLED by default (CDC_DISABLED=1) so the device enumerates as HID only after keyboard initialization — better for picky KVMs. A composite build with USB serial is available for debugging.

PS22USB — USB enumeration (CDC vs HID-only)

On ATmega32U4 (Pro Micro), USB interfaces are fixed at compile time. The stock Arduino core always includes a CDC ACM interface (USB serial) unless you build with -DCDC_DISABLED.

Default build (make compile / make upload)
  • Passes -DCDC_DISABLED via the Makefile (CDC_DISABLED=1).
  • After BootKeyboard.begin() / Consumer.begin(), the device enumerates as HID only (no USB serial from the sketch). This matches “basic HID keyboard” expectations for picky hosts.
  • Safe mode (jumper on pin 2 → GND): PS/2 is drained, HID stays off. There is no USB serial in this build — the onboard LED blinks so you can see safe mode. Remove the jumper to continue into HID mode.

Artifacts go to build/hid-only/ (default) vs build/with-serial/ (CDC_DISABLED=0) so the two firmwares never overwrite the same .hex.

If you change CDC_DISABLED and USB still behaves like the old mode, run make compile-clean once (or arduino-cli cache clean) so the Arduino core is rebuilt with the right flags.

Composite / USB serial build (make compile-serial / make upload-serial)
  • CDC_DISABLED=0 — same composite behavior as a normal Pro Micro sketch: CDC + HID.
  • Safe mode can use Serial over USB (messages, monitor), with the usual Pro Micro USB serial / iSerial behavior.

Use this when you need to debug safe mode over USB; use the default build for day-to-day “keyboard only” USB.

Uploading after HID-only builds

With CDC disabled, USB serial is not available for uploads. Use double-tap reset (or your usual Pro Micro bootloader entry) so the board appears as the bootloader device, then upload.

Makefile workflow

Install arduino-cli, cores, and libraries, then compile and upload:

.PHONY: help install compile compile-clean compile-serial upload upload-serial monitor clean list-boards list-ports
# ---- Project settings ----
SKETCH ?= PS22USB.ino
BUILD_DIR ?= build
# SparkFun Pro Micro (ATmega32U4, 5V/16MHz)
# Package: SparkFun AVR Boards
FQBN ?= SparkFun:avr:promicro:cpu=16MHzatmega32U4
# Set this when uploading, e.g.:
# make upload PORT=/dev/cu.usbmodem1101
PORT ?=
# Optional: override baud for monitor
BAUD ?= 115200
# USB CDC (USB serial) — see docs/README.md
# CDC_DISABLED=1 (default): pure HID on USB after HID init (no /dev/tty.* from sketch).
# CDC_DISABLED=0: stock composite CDC + HID (USB Serial works in safe mode; CHIDJB-style).
CDC_DISABLED ?= 1
# ---- Arduino CLI ----
ARDUINO_CLI ?= arduino-cli
# ---- Dependencies ----
BOARD_CORE ?= SparkFun:avr
LIBS ?= PS2KeyAdvanced HID-Project
# -DCDC_DISABLED removes CDC from USB descriptors (Arduino AVR core).
# Uses compiler.cpp.extra_flags only; board USB -DUSB_VID/… stays in build.extra_flags.
COMPILE_EXTRA_FLAGS :=
ifeq ($(CDC_DISABLED),1)
COMPILE_EXTRA_FLAGS += --build-property compiler.cpp.extra_flags=-DCDC_DISABLED
endif
# Separate output per variant so HID-only and serial builds never share the same .hex.
VARIANT_DIR := $(if $(filter 1,$(CDC_DISABLED)),hid-only,with-serial)
OUTPUT_DIR := $(BUILD_DIR)/$(VARIANT_DIR)
HEX := $(OUTPUT_DIR)/PS22USB.ino.hex
all: install compile upload monitor
install:
@command -v "$(ARDUINO_CLI)" >/dev/null 2>&1 || { echo "ERROR: arduino-cli not found. Install it first, then re-run 'make install'."; exit 1; }
"$(ARDUINO_CLI)" config init --overwrite >/dev/null 2>&1 || true
"$(ARDUINO_CLI)" core update-index
"$(ARDUINO_CLI)" core install "$(BOARD_CORE)"
@set -e; for lib in $(LIBS); do \
echo "Installing library: $$lib"; \
"$(ARDUINO_CLI)" lib install "$$lib" || true; \
done
@echo "Done. If 'make compile' fails due to a missing lib, tell me the error and I'll add it."
compile: $(HEX)
$(HEX): PS22USB.ino
@command -v "$(ARDUINO_CLI)" >/dev/null 2>&1 || { echo "ERROR: arduino-cli not found."; exit 1; }
"$(ARDUINO_CLI)" compile --fqbn "$(FQBN)" $(COMPILE_EXTRA_FLAGS) --output-dir "$(OUTPUT_DIR)" "$(SKETCH)"
# If USB/CDC behavior looks wrong after toggling CDC_DISABLED, run this once (slow).
compile-clean:
@command -v "$(ARDUINO_CLI)" >/dev/null 2>&1 || { echo "ERROR: arduino-cli not found."; exit 1; }
"$(ARDUINO_CLI)" compile --clean --fqbn "$(FQBN)" $(COMPILE_EXTRA_FLAGS) --output-dir "$(OUTPUT_DIR)" "$(SKETCH)"
# Composite CDC + HID (USB Serial in safe mode). Use when debugging safe mode over USB.
compile-serial:
@$(MAKE) compile CDC_DISABLED=0
upload-serial:
@$(MAKE) upload CDC_DISABLED=0
upload: $(HEX)
@command -v "$(ARDUINO_CLI)" >/dev/null 2>&1 || { echo "ERROR: arduino-cli not found."; exit 1; }
@test -n "$(PORT)" || { echo "ERROR: PORT is required. Example: make upload PORT=/dev/cu.usbmodem1101"; exit 1; }
"$(ARDUINO_CLI)" upload --fqbn "$(FQBN)" --input-dir "$(OUTPUT_DIR)" -p "$(PORT)" "$(SKETCH)"
monitor:
@command -v "$(ARDUINO_CLI)" >/dev/null 2>&1 || { echo "ERROR: arduino-cli not found."; exit 1; }
@test -n "$(PORT)" || { echo "ERROR: PORT is required. Example: make monitor PORT=/dev/cu.usbmodem1101"; exit 1; }
"$(ARDUINO_CLI)" monitor -p "$(PORT)" -c baudrate="$(BAUD)"
clean:
rm -rf "$(BUILD_DIR)"
list-ports:
"$(ARDUINO_CLI)" board list

Common commands (run inside firmware/):

Terminal window
make install # arduino-cli + SparkFun AVR core + libraries
make compile # default: HID-only → build/hid-only/PS22USB.ino.hex
make upload PORT=/dev/cu.usbmodem1101
make compile-serial # CDC + HID → build/with-serial/
make compile-clean # if USB mode “sticks” wrong after toggling CDC

Sketch source

#include <PS2KeyAdvanced.h>
#include <HID-Project.h>
// USB serial (CDC) is optional at compile time — see Makefile / docs/README.md.
// When CDC is disabled, USB enumerates as HID only (better for picky hosts / KVMs).
#if defined(USBCON) && defined(CDC_ENABLED)
#define PS22USB_HAVE_USB_SERIAL 1
#endif
#define DATAPIN 4
#define IRQPIN 3
#define SAFEPIN 2 // jumper to GND = safe mode (no HID)
PS2KeyAdvanced keyboard;
bool hidEnabled = false;
uint8_t lastSyncedPs2Locks = 0xFF;
static inline void syncPs2LocksFromHost() {
// Host LED state is the source of truth for Caps/Num/Scroll lock.
const uint8_t hostLeds = BootKeyboard.getLeds();
uint8_t ps2Locks = 0;
if (hostLeds & LED_NUM_LOCK) ps2Locks |= PS2_LOCK_NUM;
if (hostLeds & LED_CAPS_LOCK) ps2Locks |= PS2_LOCK_CAPS;
if (hostLeds & LED_SCROLL_LOCK) ps2Locks |= PS2_LOCK_SCROLL;
if (ps2Locks != lastSyncedPs2Locks) {
keyboard.setLock(ps2Locks);
lastSyncedPs2Locks = ps2Locks;
}
}
static inline void sendKeyboard(KeyboardKeycode key, bool isBreak) {
if (isBreak) BootKeyboard.release(key);
else BootKeyboard.press(key);
}
static inline void tapKeyboard(KeyboardKeycode key) {
BootKeyboard.press(key);
BootKeyboard.release(key);
}
// Apply CapsLock+Shift for letters: uppercase when CapsLock XOR Shift.
static inline void sendLetter(KeyboardKeycode key, bool isBreak, uint16_t ps2Flags) {
const bool capsOn = (ps2Flags & PS2_CAPS) != 0;
const bool shiftOn = (ps2Flags & PS2_SHIFT) != 0;
const bool wantUpper = capsOn != shiftOn;
if (isBreak) {
BootKeyboard.release(key);
if (wantUpper) BootKeyboard.release(KEY_LEFT_SHIFT);
} else {
if (wantUpper) BootKeyboard.press(KEY_LEFT_SHIFT);
BootKeyboard.press(key);
}
}
static inline bool ps2ToBootKey(uint8_t code, KeyboardKeycode &out) {
if (code >= PS2_KEY_A && code <= PS2_KEY_Z) {
out = (KeyboardKeycode)(KEY_A + (code - PS2_KEY_A));
return true;
}
if (code >= PS2_KEY_0 && code <= PS2_KEY_9) {
static const KeyboardKeycode kNumberRow[10] = {
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
};
out = kNumberRow[code - PS2_KEY_0];
return true;
}
if (code >= PS2_KEY_F1 && code <= PS2_KEY_F24) {
out = (KeyboardKeycode)(KEY_F1 + (code - PS2_KEY_F1));
return true;
}
if (code >= PS2_KEY_KP0 && code <= PS2_KEY_KP9) {
out = (KeyboardKeycode)(KEYPAD_0 + (code - PS2_KEY_KP0));
return true;
}
switch (code) {
case PS2_KEY_ESC: out = KEY_ESC; return true;
case PS2_KEY_BS: out = KEY_BACKSPACE; return true;
case PS2_KEY_TAB: out = KEY_TAB; return true;
case PS2_KEY_ENTER: out = KEY_ENTER; return true;
case PS2_KEY_SPACE: out = KEY_SPACE; return true;
case PS2_KEY_APOS: out = KEY_QUOTE; return true;
case PS2_KEY_COMMA: out = KEY_COMMA; return true;
case PS2_KEY_MINUS: out = KEY_MINUS; return true;
case PS2_KEY_DOT: out = KEY_PERIOD; return true;
case PS2_KEY_DIV: out = KEY_SLASH; return true;
case PS2_KEY_SINGLE: out = KEY_TILDE; return true; // ` / ~
case PS2_KEY_SEMI: out = KEY_SEMICOLON; return true;
case PS2_KEY_BACK: out = KEY_BACKSLASH; return true;
case PS2_KEY_OPEN_SQ: out = KEY_LEFT_BRACE; return true;
case PS2_KEY_CLOSE_SQ: out = KEY_RIGHT_BRACE; return true;
case PS2_KEY_EQUAL: out = KEY_EQUAL; return true;
case PS2_KEY_HOME: out = KEY_HOME; return true;
case PS2_KEY_END: out = KEY_END; return true;
case PS2_KEY_PGUP: out = KEY_PAGE_UP; return true;
case PS2_KEY_PGDN: out = KEY_PAGE_DOWN; return true;
case PS2_KEY_L_ARROW: out = KEY_LEFT_ARROW; return true;
case PS2_KEY_R_ARROW: out = KEY_RIGHT_ARROW; return true;
case PS2_KEY_UP_ARROW: out = KEY_UP_ARROW; return true;
case PS2_KEY_DN_ARROW: out = KEY_DOWN_ARROW; return true;
case PS2_KEY_INSERT: out = KEY_INSERT; return true;
case PS2_KEY_DELETE: out = KEY_DELETE; return true;
case PS2_KEY_PRTSCR: out = KEY_PRINTSCREEN; return true;
case PS2_KEY_PAUSE: out = KEY_PAUSE; return true;
case PS2_KEY_MENU: out = KEY_APPLICATION; return true;
// Num/Caps/Scroll are handled in loop() with tapKeyboard() before this runs.
case PS2_KEY_KP_DOT: out = KEYPAD_DOT; return true;
case PS2_KEY_KP_ENTER: out = KEYPAD_ENTER; return true;
case PS2_KEY_KP_PLUS: out = KEYPAD_ADD; return true;
case PS2_KEY_KP_MINUS: out = KEYPAD_SUBTRACT; return true;
case PS2_KEY_KP_TIMES: out = KEYPAD_MULTIPLY; return true;
case PS2_KEY_KP_DIV: out = KEYPAD_DIVIDE; return true;
case PS2_KEY_KP_EQUAL: out = KEY_PAD_EQUALS; return true;
case PS2_KEY_KP_COMMA: out = KEYPAD_COMMA; return true;
case PS2_KEY_INTL1: out = KEY_INTERNATIONAL1; return true;
case PS2_KEY_INTL2: out = KEY_INTERNATIONAL2; return true;
case PS2_KEY_INTL3: out = KEY_INTERNATIONAL3; return true;
case PS2_KEY_INTL4: out = KEY_INTERNATIONAL4; return true;
case PS2_KEY_INTL5: out = KEY_INTERNATIONAL5; return true;
case PS2_KEY_LANG1: out = KEY_LANG1; return true;
case PS2_KEY_LANG2: out = KEY_LANG2; return true;
case PS2_KEY_LANG3: out = KEY_LANG3; return true;
case PS2_KEY_LANG4: out = KEY_LANG4; return true;
case PS2_KEY_LANG5: out = KEY_LANG5; return true;
}
return false;
}
static inline bool ps2ToConsumer(uint8_t code, ConsumerKeycode &out) {
switch (code) {
case PS2_KEY_NEXT_TR: out = MEDIA_NEXT; return true;
case PS2_KEY_PREV_TR: out = MEDIA_PREVIOUS; return true;
case PS2_KEY_STOP: out = MEDIA_STOP; return true;
case PS2_KEY_PLAY: out = MEDIA_PLAY_PAUSE; return true;
case PS2_KEY_MUTE: out = MEDIA_VOLUME_MUTE; return true;
case PS2_KEY_VOL_UP: out = MEDIA_VOLUME_UP; return true;
case PS2_KEY_VOL_DN: out = MEDIA_VOLUME_DOWN; return true;
case PS2_KEY_EMAIL: out = CONSUMER_EMAIL_READER; return true;
case PS2_KEY_CALC: out = CONSUMER_CALCULATOR; return true;
case PS2_KEY_COMPUTER: out = CONSUMER_EXPLORER; return true;
case PS2_KEY_WEB_HOME: out = CONSUMER_BROWSER_HOME; return true;
case PS2_KEY_WEB_BACK: out = CONSUMER_BROWSER_BACK; return true;
case PS2_KEY_WEB_FORWARD: out = CONSUMER_BROWSER_FORWARD; return true;
case PS2_KEY_WEB_REFRESH: out = CONSUMER_BROWSER_REFRESH; return true;
case PS2_KEY_WEB_FAVOR: out = CONSUMER_BROWSER_BOOKMARKS; return true;
case PS2_KEY_POWER: out = CONSUMER_POWER; return true;
case PS2_KEY_SLEEP: out = CONSUMER_SLEEP; return true;
}
return false;
}
void setup() {
pinMode(SAFEPIN, INPUT_PULLUP);
keyboard.begin(DATAPIN, IRQPIN);
// Safe mode: keep HID completely disabled while jumper is fitted.
if (digitalRead(SAFEPIN) == LOW) {
#if defined(PS22USB_HAVE_USB_SERIAL)
// Composite firmware: USB CDC works — same enumeration as stock Pro Micro + HID later.
Serial.begin(115200);
Serial.println("SAFE MODE ACTIVE (jumper on pin 2 to GND)");
Serial.println("Remove jumper to continue booting into HID mode.");
while (digitalRead(SAFEPIN) == LOW) {
if (keyboard.available()) {
keyboard.read();
}
}
#else
// HID-only firmware: no USB serial. Blink LED so safe mode is visible.
pinMode(LED_BUILTIN, OUTPUT);
unsigned long nextToggle = 0;
bool ledOn = false;
while (digitalRead(SAFEPIN) == LOW) {
if (keyboard.available()) {
keyboard.read();
}
const unsigned long now = millis();
if (now >= nextToggle) {
nextToggle = now + 250;
ledOn = !ledOn;
digitalWrite(LED_BUILTIN, ledOn ? HIGH : LOW);
}
}
digitalWrite(LED_BUILTIN, LOW);
#endif
}
BootKeyboard.begin();
Consumer.begin();
syncPs2LocksFromHost();
hidEnabled = true;
}
void loop() {
if (!hidEnabled) return;
syncPs2LocksFromHost();
// Drain multiple queued PS/2 events per pass to avoid backlog/overflow.
uint8_t budget = 64;
while (budget-- && keyboard.available()) {
const uint16_t ev = keyboard.read();
const uint8_t code = (uint8_t)(ev & 0xFF);
if (code == 0 || code == 0xFF) continue;
const bool isBreak = (ev & PS2_BREAK) != 0;
// Lock keys: USB toggle tap each event (ignore isBreak; PS/2 may mark off as break).
switch (code) {
case PS2_KEY_CAPS:
tapKeyboard(KEY_CAPS_LOCK);
continue;
case PS2_KEY_NUM:
tapKeyboard(KEY_NUM_LOCK);
continue;
case PS2_KEY_SCROLL:
tapKeyboard(KEY_SCROLL_LOCK);
continue;
}
// Modifier keys (press/release) so chords work correctly.
switch (code) {
case PS2_KEY_L_SHIFT: sendKeyboard(KEY_LEFT_SHIFT, isBreak); continue;
case PS2_KEY_R_SHIFT: sendKeyboard(KEY_RIGHT_SHIFT, isBreak); continue;
case PS2_KEY_L_CTRL: sendKeyboard(KEY_LEFT_CTRL, isBreak); continue;
case PS2_KEY_R_CTRL: sendKeyboard(KEY_RIGHT_CTRL, isBreak); continue;
case PS2_KEY_L_ALT: sendKeyboard(KEY_LEFT_ALT, isBreak); continue;
case PS2_KEY_R_ALT: sendKeyboard(KEY_RIGHT_ALT, isBreak); continue;
case PS2_KEY_L_GUI: sendKeyboard(KEY_LEFT_GUI, isBreak); continue;
case PS2_KEY_R_GUI: sendKeyboard(KEY_RIGHT_GUI, isBreak); continue;
}
// Multimedia / "consumer" keys.
ConsumerKeycode consumerKey;
if (ps2ToConsumer(code, consumerKey)) {
if (!isBreak) Consumer.write(consumerKey);
else Consumer.release(consumerKey);
continue;
}
// Standard keyboard keys.
KeyboardKeycode bootKey;
if (ps2ToBootKey(code, bootKey)) {
// Letters need CapsLock+Shift applied for correct case.
if (code >= PS2_KEY_A && code <= PS2_KEY_Z)
sendLetter(bootKey, isBreak, ev);
else
sendKeyboard(bootKey, isBreak);
continue;
}
}
}

Build output folders

After make compile, artifacts land under:

  • firmware/build/hid-only/ — default KVM-friendly image.
  • firmware/build/with-serial/make compile-serial composite firmware.

The repository may also contain older test hex under firmware/build_cdc_test/; prefer the build/ tree from the Makefile for reproducible builds.

First boot and testing

  1. Flash the Pro Micro with make upload (or the Arduino IDE equivalent using the same FQBN and extra flags as the Makefile).
  2. Default HID-only builds: if the OS never shows a serial port, use double-tap reset to enter the bootloader before each upload.
  3. Connect a known-good PS/2 keyboard, then plug USB into the host or KVM port.
  4. Verify lock keys (Num/Caps/Scroll) sync LEDs on the keyboard when toggled from the host.
  5. Try media keys if your keyboard has them.

Files in this project (quick index)

Asset / pathPurpose
Assembled photoHero / build reference
Schematic PNGShareable schematic
PCB renderLayout preview
Gerbers zipFabrication upload (ps2usb-gerbers.zip)
Pinout photoSocket wiring visual
firmware/PS22USB.inoMain sketch (in site repo)
firmware/Makefilearduino-cli build
firmware/docs/README.mdCDC vs HID-only notes
PS2ToUSB.kicad_pro, .kicad_sch, .kicad_pcbKiCad sources (in site repo; not bundled into static dist/)

Together, these are enough to order a PCB, assemble the board, build and flash firmware, and integrate the adapter with a PS/2 keyboard and a demanding USB host or KVM.