When I first attempted this I thought it would be a difficult task. I couldn't have been more wrong. Interfacing a PS/2 type keyboard (or, as it turns out, a USB keyboard) is very simple. This page shows you how to do a simple interface using the ASCII characters, or a full interface to all of the keys on the keyboard, with suggestions on how to use the data.
The hardware is pretty simple. The PS/2 interface requires +5V and ground, data and clock, on a 6-pin female mini-DIN connector. You need a couple of pins available on your AVR, one of which needs either pin change interrupt or INTx capabilities. All AVR's have at least an INT0, many also have PCI on most if not all pins. That covers PS/2 keyboards, but what about USB keyboards? I use a USB to PS/2 converter. They are cheap, and work well.
The connectors I have found do not fit on a 0.1 inch pitch breadboard. An alternative is to either make a pigtail cable from parts, or from the female end of a PS/2 extension cable. I've done both, but prefer to make a PC board anyway, so the PC board mounted connectors work fine for me.
The code example uses a lookup table to translate a keycode into a character to be used by the application code. In it's simplest form, the keyboard interface translates scancodes into ASCII characters from 0x00 to 0x7f (0 to 127), discarding all function keys, ALT-, CTL- combinations, and cursor control keys. But first, we have to get the scancode. To do that, we have to create a software synchrounous serial receiver. A fancy name for a shift register that uses a clock supplied by the sender rather than the receiver. The PS/2 keyboard has only two signal lines: clock and data. The data is presented on the data line as a one or zero and the clock signal is lowered. The receiver clocks in the bit, and then waits for the next. When it has seen 11 bits (1 start, 8 data, 1 parity, 1 stop) it processes the received code. Again in the simplest form, we discard the start, stop, and parity bits, and only look at the scancode. The 8 data bits are shifted in LSB first. The code fragment to do that is shown here:
ISR( INT0_vect )
{
static unsigned char data;
if ( _edge == FALL )
{
if ( _bitCount < 11 && _bitCount > 2 )
{
data = ( data >> 1 ); // Shift the data over.
if ( PIND & 8 )
data |= 0x80; // Add a bit if it is a one.
}
_edge = RISE; // Ready for rising edge.
MCUCR |= 0x03; // Setup INT0 for rising edge.
}
else
{
if( --_bitCount == 0 ) // All bits received?
{
decode( data ); // Figure out what it is.
_bitCount = 11; // Ready for the next scancode.
}
_edge = FALL; // Setup routine the next falling edge.
MCUCR &= ~0x03; // Setup INT0 for the falling edge.
MCUCR |= 0x02;
}
}
In the example code above, the assumption is that INT0 is used as the clock line and PORTD3 is used as the data line. Any port pin that has pin change interrupt or INTx capability may be used for the clock, and any available port pin may be used for the data. The data received by this routine is simply an 8-bit value representing the physical key pressed. The actual ASCII value of the key must be determined by cross-referencing the scancode to an ASCII table. For instance, pressing the "a" key on a USA keyboard will return 0x1c from the keyboard. This is not an ASCII "a". In our table, we have one row which cross-references the scancode 0x1c to an ASCII "a":
unsigned char scan_to_ascii[][3] =
.
.
.
{0x1c, 'a', 'A'},
.
.
.
};
The first value is the scancode, the second is the unshifted ASCII value, the third is the shifted ASCII value. To translate the scancode, we get the shift state, look in the table until we find scan_to_ascii[n][0] == 0x1c, then grab the value which matches the shift state. For every key we want to translate, we add a line like the one above. The keys we are not interested in are left out of the table and are therefore ignored. The table is shown in RAM for simplicity, but in fact it must be in flash due to size requirements. As you add keys, it gets larger. The 128 ASCII codes are on 47 keys and would require 141 bytes. The keyboard supports many more keys than that, and in reality the table can grow to 75 rows of 5 bytes each when all keys and shift, CTL, ALT combinations are considered. That's 375 bytes total, and is more than the total RAM on some AVR's. Because it has to be in flash regardless of where it eventually ends up, and because reading flash is fast enough, it is best to just leave it there, and use program memory routines to access it.
To complete the requirements, we want to have a circular buffer that keeps the last few characters typed. The following is an example of the type of thing required.
#define MAX_KEYS 8
unsigned char keyBuffer[MAX_KEYS];
char * inPtr, * outPtr, * endPtr;
void keyInit( void )
{
inPtr = outPtr = keyBuffer;
endPtr = inPtr + MAX_KEYS;
}
void keyIn( unsigned char key )
{
*inPtr = key;
if ( ++inPtr == endPtr )
inPtr = keyBuffer;
if ( inPtr == outPtr )
if ( ++outPtr == endPtr )
outPtr = keyBuffer;
}
unsigned char keyOut( void )
{
unsigned char key = '\0';
if ( outPtr == endPtr )
outPtr = keyBuffer;
if ( outPtr != inPtr )
key = *outPtr++;
return key;
}
The board used to test the code is a simple ATmega2313 with nothing on it. If you need visual feedback, and don't have a scope, add LED's to PORTB[0:1]. Hitting keys "1", "2", or "3" puts that value on PORTB[0:1]. Hitting any other key sets it to zero.
To use the keyboard in a project, add all of the source files except main.c to your makefile, and include keyboard.h and keycodes.h in any source file that uses keyboard data.
#include "keyboard.h" #include "keycodes.h"