525 lines
16 KiB
C++
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);
|
|
}
|