/* * PS/2 Adapter for 3488 IBM terminal keyboard (French layout) * https://txmn.tk/blog/ibm-keyboard/ * * CopyLeft 2025 Pascal Engelibert * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. * * Contains substantial amount of code directly adapted from ps2dev (https://github.com/Harvie/ps2dev): * ps2dev written by Chris J. Kiick, January 2008. * ps2dev modified by Gene E. Scogin, August 2008. * ps2dev released into public domain. */ // Arduino Mega /*#define PIN_KB_CLOCK 2 #define PIN_KB_DATA 3 #define PIN_PS2_CLOCK 19 #define PIN_PS2_DATA 18*/ // Attiny402 #define PIN_KB_CLOCK PIN_PA1 #define PIN_KB_DATA PIN_PA2 #define PIN_PS2_CLOCK PIN_PA6 #define PIN_PS2_DATA PIN_PA7 // Code sent by the keyboard before a keycode, if a special key is released #define KB_RELEASE 240 // Characters received from the keyboard #define BUF_LEN 8 #define BYTE_INTERVAL_MICROS 100 //since for the device side we are going to be in charge of the clock, //the two defines below are how long each _phase_ of the clock cycle is #define CLKFULL 40 // we make changes in the middle of a phase, this how long from the // start of phase to the when we drive the data line #define CLKHALF 20 // Delay between bytes // I've found i need at least 400us to get this working at all, // but even more is needed for reliability, so i've put 1000us #define BYTEWAIT 1000 // Timeout if computer not sending for 30ms #define TIMEOUT 30 unsigned char KEYMAP[133] = { //PS2, // KB => marking new marking usage 0 , // 0 => 0x2f, // 1 => Config Config MENU 0 , // 2 => 0 , // 3 => Impr Impr 0 , // 4 => ??? 0x76, // 5 => Syst Effac ESCAPE 0 , // 6 => ??? 0x05, // 7 => F1 F1 F1 0 , // 8 => F13 F13 0x1f, // 9 => Memoir Syst SUPER 0 , // 10 => Exec Exec 0 , // 11 => Aide Aide 0 , // 12 => Effac Memoir 0x0d, // 13 => TAB TAB TAB 0x0e, // 14 => exponent 2 exponent 2 exponent 2 0x06, // 15 => F2 F2 F2 0 , // 16 => F14 F14 0x14, // 17 => Rest Rest LEFT CTRL 0x12, // 18 => Maj Maj LEFT SHIFT 0 , // 19 => < < 0x58, // 20 => Verr Maj Verr Maj CAPS LOCK 0x15, // 21 => A A A 0x16, // 22 => A1 A1 A1 0x04, // 23 => F3 F3 F3 0 , // 24 => F15 F15 0x11, // 25 => Alt Alt LEFT ALT 0x1a, // 26 => W W W 0x1b, // 27 => S S S 0x1c, // 28 => Q Q Q 0x1a, // 29 => Z Z Z 0x1e, // 30 => A2 A2 A2 0x0c, // 31 => F4 F4 F4 0 , // 32 => F16 F16 0x21, // 33 => C C C 0x22, // 34 => X X X 0x23, // 35 => D D D 0x24, // 36 => E E E 0x25, // 37 => A4 A4 A4 0x26, // 38 => A3 A3 A3 0x03, // 39 => F5 F5 F5 0 , // 40 => F17 F17 0x29, // 41 => Espace Espace SPACE 0x2a, // 42 => V V V 0x2b, // 43 => F F F 0x2c, // 44 => T T T 0x2d, // 45 => R R R 0x2e, // 46 => A5 A5 A5 0x0b, // 47 => F6 F6 F6 0 , // 48 => F18 F18 0x31, // 49 => N N N 0x32, // 50 => B B B 0x33, // 51 => H H H 0x34, // 52 => G G G 0x35, // 53 => Y Y Y 0x36, // 54 => A6 A6 A6 0x83, // 55 => F7 F7 F7 0 , // 56 => F19 F19 0x11, // 57 => Alt Alt RIGHT ALT 0x3a, // 58 => , , , 0x3b, // 59 => J J J 0x3c, // 60 => U U U 0x3d, // 61 => A7 A7 A7 0x3e, // 62 => A8 A8 A8 0x0a, // 63 => F8 F8 F8 0 , // 64 => F20 F20 0x41, // 65 => ; ; ; 0x42, // 66 => K K K 0x43, // 67 => I I I 0x44, // 68 => O O O 0x45, // 69 => A0 A0 A0 0x46, // 70 => A9 A9 A9 0x01, // 71 => F9 F9 F9 0 , // 72 => F21 F21 0x49, // 73 => : : : 0x4a, // 74 => = = = 0x4b, // 75 => L L L 0x4c, // 76 => M M M 0x4d, // 77 => P P P 0x4e, // 78 => ° ° ° 0x09, // 79 => F10 F10 F10 0 , // 80 => F22 F22 0 , // 81 => 0x52, // 82 => U GRAVE U GRAVE U GRAVE 0x5d, // 83 => MU MU MU 0x54, // 84 => ^ ^ ^ 0x55, // 85 => - - = 0x78, // 86 => F11 F11 F11 0 , // 87 => F23 F23 0x14, // 88 => Entree Entree RIGHT CTRL 0x59, // 89 => Maj Maj RIGHT SHIFT 0x5a, // 90 => ZSuiv ZSuiv ENTER 0x5b, // 91 => $ $ $ 0 , // 92 => 0 , // 93 => 0x07, // 94 => F12 F12 F12 0 , // 95 => F24 F24 0x72, // 96 => Bas Bas DOWN 0x6b, // 97 => Gauche Gauche LEFT 0 , // 98 => Trait Trait 0x75, // 99 => Haut Haut UP 0x71, // 100 => CRLF Suppr DELETE 0x69, // 101 => Inser Saut END 0x66, // 102 => RET ARR RET ARR BACKSPACE 0x70, // 103 => CR Inser INSERT 0 , // 104 => 0x69, // 105 => KP1 KP1 KP1 0x74, // 106 => Droite Droite RIGHT 0x6b, // 107 => KP4 KP4 KP4 0x6c, // 108 => KP7 KP7 KP7 0x7a, // 109 => Suppr (VIDE) PAGE DOWN 0x6c, // 110 => Dup CR HOME 0x7d, // 111 => Saut (VIDE) PAGE UP 0x70, // 112 => KP0 KP0 KP0 0x71, // 113 => KP. KP. KP. 0x72, // 114 => KP2 KP2 KP2 0x73, // 115 => KP5 KP5 KP5 0x74, // 116 => KP6 KP6 KP6 0x75, // 117 => KP8 KP8 KP8 0x77, // 118 => (VIDE) (VIDE) NUM LOCK 0x4a, // 119 => (VIDE) (VIDE) KP/ 0 , // 120 => 0x5a, // 121 => Z POS Z POS KPENTER 0x7a, // 122 => KP3 KP3 KP3 0 , // 123 => (VIDE) Z neg 0x79, // 124 => Z neg (VIDE) KP+ 0x7d, // 125 => KP9 KP9 KP9 0x7c, // 126 => (VIDE) (VIDE) KP* 0 , // 127 => 0 , // 128 => 0 , // 129 => 0 , // 130 => 0 , // 131 => (VIDE) CRLF 0x7b, // 132 => (VIDE) (VIDE) KP- }; enum ErrorCodes { EABORT = -3, ECANCEL = -2, ETIMEOUT = -1, ENOERR = 0 }; volatile unsigned int rec = 0; volatile unsigned char offset = 0; volatile unsigned char buf[BUF_LEN]; volatile unsigned char n; unsigned char kb_release = 0; bool handling_io_abort = false; // Lighter than standard library's delay void delay_millis(unsigned int ms) { unsigned long end_time = millis() + ms; while(millis() < end_time) { delayMicroseconds(10); } } /* * according to some code I saw, these functions will * correctly set the clock and data pins for * various conditions. It's done this way so you don't need * pullup resistors. */ void gohi(unsigned char pin) { pinMode(pin, INPUT); digitalWrite(pin, HIGH); } void golo(unsigned char pin) { digitalWrite(pin, LOW); pinMode(pin, OUTPUT); } char do_write(unsigned char data) { char ret; if((ret = write(data)) == EABORT && !handling_io_abort) { handling_io_abort = true; keyboard_handle(); handling_io_abort = false; } return ret; } char write(unsigned char data) { unsigned char parity = 1; golo(PIN_PS2_DATA); delayMicroseconds(CLKHALF); // device sends on falling clock golo(PIN_PS2_CLOCK); // start bit delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF); for(unsigned char i=0; i < 8; i++) { if(digitalRead(PIN_PS2_CLOCK) == LOW) { /* I/O request from host */ gohi(PIN_PS2_DATA); return EABORT; } if(data & 0x01) { gohi(PIN_PS2_DATA); } else { golo(PIN_PS2_DATA); } delayMicroseconds(CLKHALF); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF); parity ^= data & 0x01; data >>= 1; } // parity bit if(parity) { gohi(PIN_PS2_DATA); } else { golo(PIN_PS2_DATA); } delayMicroseconds(CLKHALF); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF); if(digitalRead(PIN_PS2_CLOCK) == LOW) { /* I/O request from host */ gohi(PIN_PS2_DATA); return EABORT; } // stop bit gohi(PIN_PS2_DATA); delayMicroseconds(CLKHALF); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF+BYTEWAIT); return ENOERR; } unsigned char available() { //delayMicroseconds(BYTEWAIT); return (digitalRead(PIN_PS2_DATA) == LOW) || (digitalRead(PIN_PS2_CLOCK) == LOW); } char do_read(unsigned char * value) { char ret; if((ret = read(value)) == EABORT && !handling_io_abort) { handling_io_abort = true; keyboard_handle(); handling_io_abort = false; } return ret; } char read(unsigned char * value) { // wait for data line to go low and clock line to go high (or timeout) unsigned long waiting_since = millis(); while((digitalRead(PIN_PS2_DATA) != LOW) || (digitalRead(PIN_PS2_CLOCK) != HIGH)) { if(!available()) return ECANCEL; /* Cancelled */ if(millis() - waiting_since > TIMEOUT) return ETIMEOUT; } delayMicroseconds(CLKHALF); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF); if(digitalRead(PIN_PS2_CLOCK) == LOW) { /* I/O request from host */ return EABORT; } unsigned char calculated_parity = 1; for(unsigned char i = 0; i < 8; i ++) { unsigned char recbit = digitalRead(PIN_PS2_DATA); *value |= recbit << i; calculated_parity ^= recbit; delayMicroseconds(CLKHALF); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF); if (digitalRead(PIN_PS2_CLOCK) == LOW) { /* I/O request from host */ return EABORT; } } // we do the delay at the end of the loop, so at this point we have // already done the delay for the parity bit // parity bit calculated_parity ^= digitalRead(PIN_PS2_DATA); // stop bit delayMicroseconds(CLKHALF); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); golo(PIN_PS2_DATA); golo(PIN_PS2_CLOCK); delayMicroseconds(CLKFULL); gohi(PIN_PS2_CLOCK); delayMicroseconds(CLKHALF); gohi(PIN_PS2_DATA); return calculated_parity * ECANCEL; } void ack() { delayMicroseconds(BYTE_INTERVAL_MICROS); write(0xFA); delayMicroseconds(BYTE_INTERVAL_MICROS); } void write_fa() { while(write(0xFA) != 0) delayMicroseconds(1000); } void keyboard_handle() { if(available()) { unsigned char cmd; if(!read(&cmd)) { switch(cmd) { case 0xFF: // reset ack(); // the while loop lets us wait for the host to be ready write_fa(); // send ACK while(write(0xAA) != 0) delayMicroseconds(1000); // send BAT_SUCCESS break; case 0xFE: // resend case 0xF6: // set defaults case 0xF5: // disable data reporting case 0xF4: // enable data reporting // FM ack(); break; case 0xF3: //set typematic rate ack(); if(!read(&cmd)) ack(); //do nothing with the rate break; case 0xF2: //get device id ack(); do { // ensure ID gets written, some hosts may be sensitive if(do_write(0xAB) == EABORT) continue; // this is critical for combined ports (they decide mouse/kb on this) if(do_write(0x83) == EABORT) continue; break; } while(!handling_io_abort); break; case 0xF0: // set scan code set ack(); if(!read(&cmd)) ack(); //do nothing with the rate break; case 0xEE: // echo //ack(); delayMicroseconds(BYTE_INTERVAL_MICROS); write(0xEE); delayMicroseconds(BYTE_INTERVAL_MICROS); break; case 0xED: //set/reset LEDs //ack(); write_fa(); if(!read(&cmd)) { write_fa(); } } } } } // signal from keyboard void isr_kb() { unsigned char d = digitalRead(PIN_KB_DATA); if(offset == 0 && d != 0) return; rec |= ((unsigned int) d) << offset; offset ++; if(offset >= 11) { offset = 0; if(rec != 0) { buf[n] = rec >> 1; rec = 0; } n = (n + 1) % BUF_LEN; } } void setup() { pinMode(PIN_KB_CLOCK, INPUT); pinMode(PIN_KB_DATA, INPUT); gohi(PIN_PS2_CLOCK); gohi(PIN_PS2_DATA); delay_millis(200); write(0xAA); attachInterrupt(digitalPinToInterrupt(PIN_KB_CLOCK), isr_kb, FALLING); } void loop() { //Handle PS2 communication and react to keyboard led change //This should be done at least once each 10ms keyboard_handle(); if(n > 0) { noInterrupts(); for(unsigned char i = 0; i < n; i ++) { unsigned char c = buf[i]; if(c == KB_RELEASE) { kb_release = 1; } else { unsigned char released = 0; unsigned char press_and_release = 0; switch(c) { case 17: case 18: case 20: case 25: case 57: case 88: case 89: if(kb_release) { kb_release = 0; released = 1; } break; default: if(c > 132) c = 0; press_and_release = 1; } unsigned char cc = KEYMAP[c]; if(cc != 0) { send: do { switch(c) { case 9: // SUPER case 57: // RIGHT ALT case 88: // RIGHT CTRL case 96: // UP case 97: // DOWN case 99: // LEFT case 100: // DELETE case 103: // INSERT case 106: // RIGHT case 119: // KP/ case 121: // Z POS (KP ENTER) // It's a special character if(do_write(0xe0) == EABORT) continue; } if(released && do_write(0xf0) == EABORT) continue; if(do_write(cc) == EABORT) continue; break; } while(!handling_io_abort); if(press_and_release) { press_and_release = 0; released = 1; goto send; } } } } n = 0; interrupts(); } delayMicroseconds(1000); }