ibm-ps2-keyboard/ibm-ps2-keyboard.ino
2025-02-05 16:38:59 +01:00

525 lines
16 KiB
C++

/*
* 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);
}