External PS/2 keyboard for Gradiente Expert MSX computer

Similar projects worth following
The Gradiente Expert was a MSX1 computer manufactured in Brasil in the 80's. It is still possible to buy these computers online but many of them have lost their keyboard.
This project describes an adapter to connect a PS/2 keyboard (or USB keyboard in fallback mode) to the Expert using an ATmega328 transplanted from an Arduino board.

The MSX keyboard is arranged as a matrix of up to 11 lines by 8 rows. The lines are selected bay the 4 lsbs on PPI port C (that feeds a BCD-to-Decimal decoder). The rows are read on the port PPI port B.

The reading of a given line is made by writing to port C then read on port B.
;  register A holds the desired line.

OUT (0AAh),A
IN A,(0A9H)

The technique used on this adapter is to detect a pin change caused by the Z80 OUT instruction and answer in time for the Z80 to sample the data line during the IN instruction.

Z80 Input/Output Timing in MSX

The IN/OUT instruction takes 11 clock cycles. In MSX it takes one extra cycle for M1 cycle:

INSTRUCTION   BYTES   M1         M2        M3  
OUT (n),A     2       OCF(4+1)   OD(3)     PW(4)  
IN A,(n)      2       OCF(4+1)   OD(3)     PR(4)

During M1 the Z80 fetches the instruction.

During M2 the Z80 fetches the operand in the second byte

During M3, finally, the Z80 reads/writes on the I/O pins.

The time interval available to react begins on the half of T2 on the OUT instruction to the half of T4 on the IN instruction, or in numbers:

  • 2,5 clock cycles remaining form cycle M3 of OUT instruction
  • 5 clock cycles from the M1 cycle of IN instruction ('Opcode Fetch')
  • 3 clock cycles from the M2 cycle ('Operand Data' fetch )
  • 3 clock cycles from the M3 cycle ('Data Read') as data should be ready to be sample on the middle of T3

Doing the math, we have 13,5 cycles which on a Z80 machine running at 3.58MHz  is the equivalent of 3,77us

  • 1 × ATMega328p Microprocessors, Microcontrollers, DSPs / ARM, RISC-Based Microcontrollers
  • 3 × 10k THT resistor 1/8 watt
  • 1 × 100nf Ceramic Capacitor
  • 2 × 22pf ceramic capacitor
  • 1 × 16.0MHz Frequency Control / Crystals

View all 15 components

  • PS/2 and USB in (fallback mode) tests

    danjovic07/09/2021 at 14:31 0 comments

    Finally got some time to record some videos of the keyboard adapter working in both native PS/2 keyboard and with a USB keyboard on PS/2 fallback mode.

    Box Opened
    Box Opened

    Native PS/2 keyboard

    USB in PS/2 fallback mode

  • Rise time with pullups

    danjovic04/10/2021 at 15:00 0 comments

    I was intrigued with some interrupt response time figures. Theoretically I should have figures slightly over 1us (and indeed I have) but sometimes the response time scaled up from 2.0 to 3.5 us.

    Then I realized that the longest response times were all on rising edges!

    What happens is that I am using DDR register to emulate open collector outputs and for that reason the fall times are within expected, but the rise time will depend upon the R-C constant formed by the pullup resistors on the MSX computer along with the parasitic capacitance (predominantly from the wiring).

    I should have seen that before!

  • "Ported" the code to Plain C

    danjovic04/10/2021 at 00:47 0 comments

    I have just ported the source code toplain C which required me to practically rewrite the PS/2 keyboard library. Besides that it was necessary to create the prototype of various functions and a main() function

    int main (void) {
       for (;;) loop ();

     It was possible of course to rename the loop() function and call setup() from inside.

    Also added a makefile. It was necessary to change one DLL (msys-1.0.dll) to make it possible to compile the code in windows 10 .

    The last modification was to force the USART to turn off, otherwise the pins PD0 and PD1 would not respond to the writes.

      UCSR0B = 0; // disable USART

  • Basic Release

    danjovic04/09/2021 at 04:10 0 comments

    Basic Release is ready on GitHub, developed on Arduino IDE. I should migrate to plain C code now that I have finished to debug my own PS/2 kebyboard library.

    By the way I spent some time to find something that now seems obvious but hard to observe on the logic analyzer that is the rise time of the wired or connections for the PS/2 keyboard:

    In a given moment during the beginning of the communication with the keyboard it is necessary to release the CLOCK line then wait for the keybard to pull down the CLOCK line.

    When that operation is done on the ps2.h library, there is no explicit delay after releasing the line, because the Arduino (wiring) DigitalWrite command takes a while to process.

      golo(_ps2clk);  _delay_us(300);
      golo(_ps2data); _delay_us(10);
      gohi(_ps2clk);                  // start bit
    	while (digitalRead(_ps2clk) == HIGH); /* wait for device to take control of clock */

    But the line takes some time to recover, 440 ns from  0 to approximately 3.0 Volts (positive threshold for the ATMega328):

    To correct that problem I have added a 5 microseconds delay after the release of the CLK line to give such line some time to recover.
      // 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.

    Worth to mention that the functions above are indeed macros dealing directly with the microcontroller registers.

    // Hardware definition
    #define DAT_DDR  DDRB
    #define DAT_PIN  PINB
    #define DAT_PORT PORTB
    #define DAT_BIT  0
    #define CLK_DDR  DDRB
    #define CLK_PIN  PINB
    #define CLK_PORT PORTB
    #define CLK_BIT  1
    #define dropDAT()    do { DAT_DDR  |=  (1 << DAT_BIT); DAT_PORT &= ~(1 << DAT_BIT); } while(0)
    #define dropCLK()    do { CLK_DDR  |=  (1 << CLK_BIT); CLK_PORT &= ~(1 << CLK_BIT); } while(0)
    #define releaseDAT() do {  DAT_DDR  &= ~(1 << DAT_BIT); DAT_PORT |=  (1 << DAT_BIT); } while (0)
    #define releaseCLK() do {  CLK_DDR  &= ~(1 << CLK_BIT); CLK_PORT |=  (1 << CLK_BIT); } while (0)
    #define readDAT()  (DAT_PIN & (1 << DAT_BIT))
    #define readCLK()  (CLK_PIN & (1 << CLK_BIT))
    #define waitDATrise()  do {} while (!readDAT())
    #define waitDATfall()  do {} while ( readDAT())
    #define waitCLKrise()  do {} while (!readCLK())
    #define waitCLKfall()  do {} while ( readCLK())

  • Shrinking the ISR

    danjovic04/06/2021 at 02:37 0 comments

    it was necessary to shrink the pin change irq to fit within the timing necessary to attend the Z80.

    Doing the Math I should be spending 1us to output the code, counting the IRQ latency, but I have seen some figures of up to 3.25us which is barely enough.

    It should be possible to increase this margin by changing the crystal to 20MHz, but I am testing with an arduino board (at 16MHz).

    The code uses some tricks to save clock cycles like saving the registers r26 and r27 on general purpose I/O registers available in ATMega328, and using the __zero_reg__ to store the flags

    volatile uint8_t zero = 0;
    ISR (PCINT1_vect, ISR_NAKED) {
      asm volatile (                     // 7 instrucoes de latencia até aqui
        "in __zero_reg__,__SREG__ \n\t"  // 1 Salva registrador de Status
        "out %[_GPIOR1],r26\n\t"         // 1 salva registro em 1 ciclo
        "out %[_GPIOR2],r27\n\t"         // 1 salva registro em 1 ciclo
        "ldi r27,hi8(Keymap)\n\t"        // 1 Ponteiro X = endereço de Keymap  
        "in r26,%[_PINC] \n\t"           // 1
        "subi r26, lo8(-(Keymap)) \n\t"  // 1 
        "ld 26,X \n\t"                   // 2 lê coluna correspondente do mapa de teclas
        "out %[_DDRD] ,r26 \n\t"         // 1 escreve na porta B da PPI
                                         // até aqui 16 instruções (1us @16Mhz)
        "in r26, %[_GPIOR1] \n\t"
        "in r27, %[_GPIOR2] \n\t"
        "sbi %[_PCIFR],1 \n\t"           // reset interrupt bit
        "out __SREG__,__zero_reg__ \n\t" // restaura registrador de Status
        "lds __zero_reg__, zero \n\t"
        "reti \n\t"
        ::[_PINC] "I" (_SFR_IO_ADDR(PINC)  ),
        [_DDRD]   "I" (_SFR_IO_ADDR(DDRD)  ),
        [_PORTB]  "I" (_SFR_IO_ADDR(PORTB) ),    
        [_GPIOR1] "I" (_SFR_IO_ADDR(GPIOR1)),
        [_GPIOR2] "I" (_SFR_IO_ADDR(GPIOR2)), 
        [_PCIFR]  "I" (_SFR_IO_ADDR(PCIFR) )

View all 5 project logs

  • 1
    Prepare the cable

    Add a solder blob on each pin of the DIN-13 connector

    Solder the wires following the color code/pinout on the schematics. Start from the lower and use heat shrink tubing on each wire.

    Solder the cable shield to the connector shell taking care to not overheat and melt the cable insulation

    Close the DIN13 connector.

    Solder the wires on the DA-15 male connector. Add a short wire to the cable shield (that will be soldered on pin 14. Use heat shrink tubing on each wire. Add heat shrink tubing around the cable to insulate the shield and get a better appearance.

View all instructions

Enjoy this project?



Glmaxter wrote 11/08/2023 at 02:31 point

Will this work on a Sony hb-f500 msx2?

  Are you sure? yes | no

Augusto Baffa wrote 06/29/2022 at 03:35 point

does it run at 16mhz? I'd like to try it on a custom board.

  Are you sure? yes | no

danjovic wrote 06/29/2022 at 22:32 point

Hi Augusto, Sure! I have chosen 16MHz to be able to use an ordinary Arduino board as a prototype.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates