Skip to content

Commit

Permalink
Fix keyboard initialization.
Browse files Browse the repository at this point in the history
  • Loading branch information
dc740 committed Nov 30, 2022
1 parent 637fdd4 commit d0be819
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 50 deletions.
52 changes: 45 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,65 @@ which pin triggered the interrupt.
| 10 | X6 | Output | C6 |
| 17 | X7 | Output | C7 |


| PS2 Pin | Teensy++ 2.0 (AT90USB1286) port |
|:-----:|:------:|:------:|
| DATA | B2 |
| CLOCK | B1 |

5V and GND pins:
Please take GND from the properly labeled pins in the MSX and NOT the header!
There is no GND on the keyboard header. It looks like it, but it's not.
5V can be taken from the header without problems.

# Features:

* Simulates all key presses
* Each trigger works in around 1us.
* It powers down itself if the keyboard is missing to reduce power usage.
* Instantly updates the MSX if a currently selected column is updated

# How it works:

When Y0..Y8 from the keyboard header go low, each fire an interrupt in our AVR. Then the interrupt sets the bits on PORTC to
I got the idea while trying to fix a broken keyboard membrane, and confirmed it should work when I found someone else who already
did that too:
https://caro-su.translate.goog/msx/kbd4msx.htm?_x_tr_sch=http&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp

When Y0..Y8 from the keyboard header go low, each fire a falling edge interrupt in our AVR. Then the interrupt sets the bits on PORTC to
tell the MSX which keys are pressed or not.
The hardest part is to get the timing right. The Arduino takes 3.5us after the interrupt is fired until it sets the port with
our pressed keys, so I had to experiment with a few workarounds to overcome this problem, since it would skip the entire first
row if I didn't fix the 3.5us delay. In the end I reimplemented it in C heavily based on another project (listed below).

In order for the instant update to work, you need to take care of the timings on raising edge, and update the MSX
with the currently selected column (if any). I wrote a macro to prevent collisions and ensure we never set the column
after the raising edge interrupt fires, since this would mean that the PORTC would have the column value when it
actually should be high, to prevent wrong keypresses during Y9 and Y10 (when the BIOS functionality is used).


I got the idea while trying to fix a broken keyboard membrane, and confirmed it should work when I found someone else who already
did that too:
https://caro-su.translate.goog/msx/kbd4msx.htm?_x_tr_sch=http&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp

The code is heavily based on this one. I originally implemented all the same in Arduino, but the performance was so bad
The code was heavily based on the project below, but I eventually rewrote most of it to fix the speed and compatibility.
The Sony HB 10p and 20p also don't have any external connectivity for the keyboard and don't use a decoder like other models.
They select the matrix column directly from the S3527 to the keyboard membrane. I'm not sure what the other models use, but every
single implementation I saw used a decoder to select the column on the membrane or had an external port that used a decoder too.
I originally implemented all the same in Arduino, but the performance was so bad.
that I started looking at other options, and this project did (almost) everything I wanted to do, so I rewrote the interrupt
handlers, parts of the logic, and reused the PS2 library:
https://hackaday.io/project/178358-exps2
https://github.com/Danjovic/MSX/tree/master/EXPS2/firmware/EXPS-2

# Layout

Adding an spanish layout would essentially mean to lose most of the special characters on the original keyboard.
So I created a layout that matches the original one, but on an spanish keyboard.

If anyone wants to add a new layout, please feel free to send a Pull Request.
Things to consider before starting such a project:
Each key on the spanish keyboard does NOT have a 1-1 match on the MSX and viceversa.
You will need to map a scan code from the PS2 keyboard (or USB if you have a host enabled device) and map it to
several MSX keys.
Per last statement, each keypress on the PS2 keyboard can result in SHIFT, GRAPH, CODE keys + another character, all in one single press,
but most important, the shift and Alt GR status from the PS2 keyboard must be completely independent from the same keys on the MSX keyboard.
Some examples:
'{' on the spanish keyboard is produced by pressing 'Alt Gr' + '´' key. And needs to be translated to 'SHIFT' + '[' on the MSX matrix.
'`' on the spanish keyboard is produced by pressing '`' key. And needs to be translated to 'CODE' + '´' on the MSX matrix.
'^' on the spanish keyboard is produced by pressing 'SHIFT'+'`' key. And needs to be translated to 'CODE' + 'SHIFT' + '´' on the MSX matrix.

47 changes: 6 additions & 41 deletions plainC/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ bool BRK = false;
// | | ' \ _/ -_) '_| '_| || | '_ \ _(_-<
// |_|_||_\__\___|_| |_| \_,_| .__/\__/__/
// |_|
// TODO: move this to a .S file! they are too long to be here
// and also too long for a macro, but they are basically all the same
// except for the pin change interrupt which only changes a few lines.
volatile uint8_t zero = 0;

// COLUMN_STATUS[0]
Expand Down Expand Up @@ -589,8 +592,10 @@ void setup(void) {
DDRC = 0xFF; // PORTC (10 to 18 in teensy++ 2.0) is the column output
PORTC = 0xFF; // write high on all ports
UCSR1B = 0; // disable USART

//keyboard initialization
if (!initalizeKeyboard()){
_delay_ms(800); //wait for the keyboard to power up
if (!initPS2()){
#ifdef DEBUGMODE
debug ("No keyboard!");
#endif
Expand Down Expand Up @@ -765,46 +770,6 @@ inline uint8_t* getColumn(uint8_t column)
return selectedColumn;
}

//
// Test for connection and Initialize Keyboard
// Returns false on success
// and true on error
//
bool initalizeKeyboard( void )
{
uint8_t ack;
_delay_ms(1000); //It takes around 750ms to start
// First... lets see if it's connected
releaseCLK();
releaseDAT();
_delay_us(50);
// http://www.burtonsys.com/ps2_chapweske.htm
// 1) Bring the Clock line low for at least 100 microseconds.
dropCLK();
_delay_us(150);
// 2) Bring the Data line low.
dropDAT();
_delay_us(10);
// 3) Release the Clock line.
releaseCLK();
_delay_us(5); // give some time for the line to raise
// 4) Wait for the device to bring the Clock line low.
long timeout = 50000; // we loop to make up some time
do {
timeout--;
} while ( (readCLK()) && (timeout));
if (timeout) {
dropDAT();
_delay_ms(2000); // lets cancel everything
writePS2(0xff); // send reset code
ack = readPS2(); // byte, kbd does self test, returns ACK
ack = readPS2(); // another ack when self test is done
if (ack == 0xAA) { //0xAA= PS2_KC_BAT success code
return true;
}
}
return false;
}
// not really atomic, but works for us
// 1 __tmp_reg__ holds the value
// 2 disable interrupts
Expand Down
99 changes: 99 additions & 0 deletions plainC/myPS2.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,107 @@
*/
#include <avr/io.h>
#include <util/delay.h>
#include <stdbool.h>
#include "myPS2.h"

//
// Pretty much the same as writePS2, except with a timeout
// and verification.
// Checks that the keyboard is connected and correctly initialized
// returns true on success. false otherwise
bool initPS2() {
uint8_t ack;
uint8_t data=0xff; //reset code
uint8_t i;
uint8_t parity = 1;

// prepare for transmit
releaseCLK();
releaseDAT();
_delay_us(200);

// http://www.burtonsys.com/ps2_chapweske.htm
// 1) Bring the Clock line low for at least 100 microseconds.
dropCLK();
_delay_us(150);

// 2) Bring the Data line low.
dropDAT();
_delay_us(10);

// 3) Release the Clock line.
releaseCLK();

_delay_us(5); // give some time for the line to raise

// 4) Wait for the device to bring the Clock line low.
long timeout = 100000; // we loop to make up some time
do {
timeout--;
} while ( (readCLK()) && (timeout)); // same as waitCLKfall()
if (timeout==0) {
return false; // Keyboard not present
}

for (i = 0; i < 8; i++)
{
// 5) Set/reset the Data line to send the first data bit
if (data & 0x01)
{
releaseDAT();
parity++;
} else {
dropDAT();
}
// 6) Wait for the device to bring Clock high.
waitCLKrise();

// 7) Wait for the device to bring Clock low.
waitCLKfall();

data >>= 1;

// 8) Repeat steps 5-7 for the other seven data bits
} // for
//
// and the parity bit
if (parity & 0x01)
{
releaseDAT();
} else {
dropDAT();
}

waitCLKrise();
waitCLKfall();

// 9) Release the Data line.
releaseDAT();
_delay_us(10);

// 10) Wait for the device to bring Data low.
waitDATfall();

// 11) Wait for the device to bring Clock low.
waitCLKrise();

// 12) Wait for the device to release Data and Clock
waitDATrise();
waitCLKrise();

// Hold data line low to hold keyboard until next action
dropDAT();

ack = readPS2(); // byte, kbd does self test, returns ACK
ack = readPS2(); // another ack when self test is done
if (ack == 0xAA) { //0xAA= PS2_KC_BAT success code
return true;
} else {
return false;
}
}


//
// Send a byte to the PS/2 device

Expand Down
10 changes: 8 additions & 2 deletions plainC/myPS2.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@
#define waitCLKrise() do {} while (!readCLK())
#define waitCLKfall() do {} while ( readCLK())

// Send one byte to the device
// Pretty much the same as writePS2, except with a timeout
// and verification.
// Checks that the keyboard is connected and correctly initialized
// returns true on success. false otherwise
bool initPS2();

// Send one byte to the device. Blocking implementation.
void writePS2(uint8_t data);

// Receive one byte from the device
// Receive one byte from the device. Blocking implementation.
uint8_t readPS2(void);

0 comments on commit d0be819

Please sign in to comment.