My little TI99-4A matrix keyboard testing project has evolved into a cool I2C keyboard that may find use in any micro controller hobby application. I built a prototype circuit and wrote software running on Atmel AT89S52 micro that scans a full matrix keyboard of a retro computer and sends ANSI ASCII codes via I2C bus. Why settle for a keypad with just a few keys, when you can have a full ASCII terminal connected to your micro controller project?
Any matrix keyboard can be used after modification of the keyboard scanning algorithm and key codes table. Philosophy remains the same.
Device works in master mode. I tested it with Arduino Uno board since it has I2C port and can work in slave mode.
Wiring diagram
(8051 controller only, connecting to Arduino is trivial, I'll explain in a minute):
I2C bus consists of 2 lines, SDA (data) and SCL (clock).
SDA line goes to Arduino's input pin A4, SCL to Arduino's pin A5.
I power the keyboard controller circuit from Arduino board, so there is a total of 4 lines going from keyboard controller to Arduino: SDA, SCL, VCC and GND.
Here is the 8051 code (SDCC):
/*
* Project: TI99-4A I2C keyboard.
* Module: ti994a
* Author: Marek Karcz
* Purpose: TI99-4A keyboard with micro connected to I2C.
* I2C input device. Works in MASTER mode.
*
* Hardware:
* Atmel 8051 compatible microcontroller AT89S52.
* 1602 LCD display
* TI99-4A matrix keyboard (ports P2, P3).
* I2C line drivers (NPN transistors, base and pull-up resistors, NAND gates 74LS00).
* NAND gates - incoming SDA, SCK from I2C bus to data/clock in pins.
* NPN transistors - outgoing SDA, SCL signals from 8051 data/clock out pins to I2C bus.
*
* Keyboard to port pin connections:
*
* TI99-4a pin# Pn.b
* --------------------------
* 11 P3.7
* 10 P3.6
* 3 P3.5
* 7 P3.4
* 2 P3.3
* 1 P3.2
* 4 P3.1
* 5 P3.0
*
* 6 P2.6
* 8 P2.5
* 9 P2.4
* 15 P2.3
* 14 P2.2
* 13 P2.1
* 12 P2.0
*
* =================================
*
* I2C bus interface:
*
* P1.3 - sda (data) out inverted
* P1.4 - sda (data) in inverted
* P1.5 - scl (clock) out inverted
* P1.6 - scl (clock) in inverted
*
* ================================
*
* TO DO:
*
* 1) Auto-repeat.
* 2) Cover all CTRL ANSI codes.
* 3) Receive characters from I2C bus and display on LCD.
* I am not sure if this is practical. Perhaps separate I2C display on I2C bus, independent
* from this microcontroller would work better.
*
*/
#include <at89x52.h>
#include <stdlib.h>
#include <string.h>
// keil -> sdcc
#define sbit __sbit
#define code __code
#define using __using
#define interrupt __interrupt
#define _nop_() __asm NOP __endasm
typedef unsigned char BYTE;
typedef unsigned int WORD;
typedef sbit BOOL ;
#define rs P1_0
#define rw P1_1
#define ep P1_2
#define KBP1 P3
#define KBP2 P2
// NOTE: I2C pins are inverted
#define SDA_OUT P1_3
#define SDA_IN P1_4
#define SCL_OUT P1_5
#define SCL_IN P1_6
#define I2C_HIGH 0
#define I2C_LOW 1
BYTE code dis1[] = {"I2C keyboard 1.0"};
BYTE code dis2[] = {"(C) Marek Karcz "};
BYTE code dis3[] = {"I2C line busy..."};
BYTE dispbuf[17];
BYTE keycode=0;
BYTE keysent=0;
BYTE row=0;
BYTE column=0;
BYTE code kbmatrix[8][7] =
{
{11, 43, 42, 41, 40, 22, 0},
{47, 31, 30, 29, 28, 32, 0},
{33, 20, 19, 18, 17, 21, 0},
{ 0, 9, 8, 7, 6, 10, 0},
{48, 2, 3, 4, 5, 1, 45},
{44, 24, 25, 26, 27, 23, 0},
{46, 13, 14, 15, 16, 12, 0},
{ 0, 36, 37, 38, 39, 35, 0}
};
BOOL bShiftOn = 0;
BOOL bCtrl = 0;
BOOL bFunc = 0;
BOOL bLock = 0;
void delay(BYTE ms);
void init_ports(void)
{
KBP1 = 0x00;
KBP2 = 0xFF;
P1 = 0xFF;
SDA_IN = 1;
SCL_IN = 1;
SDA_OUT = 0;
SCL_OUT = 0;
}
void init_dispbuf(void)
{
BYTE i=0;
for(i=0; i<16; i++)
{
dispbuf[i]=32;
}
dispbuf[i]=0;
}
void init_vars(void)
{
init_dispbuf();
keycode=0xFF;
row=0;
column=0;
}
void delay(BYTE ms)
{
BYTE i;
while(ms--)
{
for(i = 0; i< 250; i++)
{
_nop_();
_nop_();
_nop_();
_nop_();
}
}
}
/*
* LCD 16x2 driver functions.
*/
BOOL lcd_bz()
{
BOOL result;
rs = 0;
rw = 1;
ep = 1;
_nop_();
_nop_();
_nop_();
_nop_();
result = (BOOL)(P0 & 0x80);
ep = 0;
return result;
}
void lcd_wcmd(BYTE cmd)
{
while(lcd_bz());
rs = 0;
rw = 0;
ep = 0;
_nop_();
_nop_();
P0 = cmd;
_nop_();
_nop_();
_nop_();
_nop_();
ep = 1;
_nop_();
_nop_();
_nop_();
_nop_();
ep = 0;
}
void lcd_pos(BYTE pos)
{
lcd_wcmd(pos | 0x80);
}
void lcd_wdat(BYTE dat)
{
while(lcd_bz());
rs = 1;
rw = 0;
ep = 0;
P0 = dat;
_nop_();
_nop_();
_nop_();
_nop_();
ep = 1;
_nop_();
_nop_();
_nop_();
_nop_();
ep = 0;
}
void lcd_init()
{
lcd_wcmd(0x38);
delay(1);
lcd_wcmd(0x0c);
delay(1);
lcd_wcmd(0x06);
delay(1);
lcd_wcmd(0x01);
delay(1);
}
void lcd_clear(BYTE pos)
{
BYTE i=0;
lcd_pos(pos);
for(i=0; i<16; i++)
{
lcd_wdat(32);
}
}
void lcd_text(BYTE pos, BYTE txt[])
{
BYTE i=0;
lcd_clear(pos);
lcd_pos(pos);
i=0;
while((int)txt[i] != (int)'\0')
{
lcd_wdat(txt[i]);
i++;
}
}
/*
* TI99-4A keyboard driver functions.
*/
/* Scans keyboard.
* Returns 0 if no key pressed.
* Returns row# (1-8) if key pressed.
* Sets global flags indicating special/control key pressed.
* Column is calculated from the read key code.
*/
BYTE read_kb(void)
{
BYTE i=0;
BYTE j=0;
BYTE ret=0;
BYTE keyscan=0;
BYTE pattern=0x01;
BYTE col=0;
for (i=0,pattern=0x01; pattern!=0; pattern<<=1,i++)
{
KBP2 = 0xFF;
KBP1 = ~pattern;
keyscan = KBP2;
if(keyscan!=0xFF)
{
init_dispbuf();
for(j=1,col=0; j!=0; j<<=1,col++)
{
if (~keyscan & j)
{
if (kbmatrix[i][col] == 44) bShiftOn = 1;
else if (kbmatrix[i][col] == 45) { bShiftOn = 1; bLock = 1; }
else if (kbmatrix[i][col] == 46) bCtrl = 1;
else if (kbmatrix[i][col] == 48) bFunc = 1;
else { row = i; ret = i+1; column = col; keycode = kbmatrix[row][column]; }
}
}
}
delay(2);
}
// key released, reset flags
if (ret == 0)
keysent = bShiftOn = bLock = bCtrl = bFunc = 0;
return ret;
}
/*
* I2C bus communication driver functions.
*
* NOTE: in/out data and clk signals inverted
*
*/
/*Send start condition*/
void I2C_Start(void) /*Initial conditions*/
{
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SDA_OUT = I2C_LOW;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_LOW;
}
void I2C_Stop(void) /*Stop condition*/
{
SCL_OUT = I2C_LOW;
SDA_OUT = I2C_LOW;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_HIGH;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SDA_OUT = I2C_HIGH;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
}
BOOL I2C_Ack(void) /* Receive acknowledge bit */
{
SDA_IN = 1;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_HIGH;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_LOW;
return (SDA_IN == I2C_LOW);
}
void I2C_NoAck(void) /* Send acknowledge bit */
{
SDA_OUT = I2C_LOW;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_HIGH;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_LOW;
}
void I2C_Send(BYTE d) /*Subroutine to send data, d is required to send data*/
{
BYTE BitCounter=8; /*Median control*/
BYTE temp; /*Intermediate variable control*/
do
{
temp=d;
SCL_OUT = I2C_LOW;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
if((temp&0x80)==0x80)/* If the highest bit is 1*/
SDA_OUT = I2C_HIGH;
else
SDA_OUT = I2C_LOW;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_HIGH;
temp=d<<1; /*RLC*/
d=temp;
BitCounter--;
}while(BitCounter);
SCL_OUT = I2C_LOW;
}
BYTE I2C_Read(void) /*Read one byte of data, and returns the byte value*/
{
BYTE temp=0;
BYTE temp1=0;
BYTE BitCounter=8;
SDA_IN = 1;
do{
SCL_OUT = I2C_LOW;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
SCL_OUT = I2C_HIGH;
_nop_ ();
_nop_ ();
_nop_ ();
_nop_ ();
if(SDA_IN == I2C_HIGH) /*If Sda=1;*/
temp=temp|0x01; /*The lowest temp 1*/
else
temp=temp&0xfe; /*Otherwise, the lowest temp clear 0*/
if(BitCounter-1)
{ temp1=temp<<1;
temp=temp1;
}
BitCounter--;
}while(BitCounter);
return(temp);
}
BOOL I2C_LineBusy(void)
{
return (SDA_IN != I2C_HIGH || SCL_IN != I2C_HIGH);
}
void lcdmsg(BYTE msg[], BYTE iter, BYTE dl)
{
BYTE n;
lcd_text(0, msg);
for(n=0; n<iter; n++)
{
delay(dl);
}
lcd_text(0, dis1);
}
void I2C_SendKey(BYTE kc)
{
if (!I2C_LineBusy())
{
if (kc != keysent) // debounce
{
I2C_Start();
I2C_Send(4<<1); // slave receiver address
// shift left for 7 bit addr. + LSB as a R/W bit
// LSB = 0 - WRITE
I2C_Ack();
I2C_Send(kc);
I2C_Ack();
I2C_Stop();
keysent = kc;
}
}
else
{
lcdmsg(dis3, 8, 255); // Line busy message to LCD
lcd_text(0, dis1);
}
}
/*
* Convert keyboard scan code to ANSI ASCII code.
* (more or less :-) )
*/
BYTE convKeyCode2Char(BYTE kc)
{
BYTE ch = 0;
switch (kc)
{
case 1: ch = ((bShiftOn) ? '!' : '1'); break;
case 2: ch = ((bShiftOn) ? '@' : '2'); break;
case 3: ch = ((bShiftOn) ? '#' : '3'); break;
case 4: ch = ((bShiftOn) ? '$' : '4'); break;
case 5: ch = ((bShiftOn) ? '%' : '5'); break;
case 6: ch = ((bShiftOn) ? '^' : '6'); break;
case 7: ch = ((bShiftOn) ? '&' : '7'); break;
case 8: ch = ((bShiftOn) ? '*' : '8'); break;
case 9: ch = ((bShiftOn) ? '(' : '9'); break;
case 10: ch = ((bShiftOn) ? ')' : '0'); break;
case 11: ch = ((bShiftOn) ? '+' : '='); break;
case 12: if (bCtrl) ch = 17; /* CTRL-Q */ else ch = ((bShiftOn) ? 'Q' : 'q'); break;
case 13: if (bFunc) ch = '~'; else ch = ((bShiftOn) ? 'W' : 'w'); break;
case 14: if (bFunc) ch = 128; /* up arrow */ else ch = ((bShiftOn) ? 'E' : 'e'); break;
case 15: if (bFunc) ch = '['; else ch = ((bShiftOn) ? 'R' : 'r'); break;
case 16: if (bFunc) ch = ']'; else ch = ((bShiftOn) ? 'T' : 't'); break;
case 17: ch = ((bShiftOn) ? 'Y' : 'y'); break;
case 18: if (bFunc) ch = '_'; else ch = ((bShiftOn) ? 'U' : 'u'); break;
case 19: if (bFunc) ch = '?'; else ch = ((bShiftOn) ? 'I' : 'i'); break;
case 20: if (bFunc) ch = '\''; else ch = ((bShiftOn) ? 'O' : 'o'); break;
case 21: if (bFunc) ch = '"'; else ch = ((bShiftOn) ? 'P' : 'p'); break;
case 22: ch = ((bShiftOn) ? '-' : '/'); break;
case 23: if (bFunc) ch = '|'; else ch = ((bShiftOn) ? 'A' : 'a'); break;
case 24: if (bCtrl) ch = 19; /* CTRL-S */
else {if (bFunc) ch = 129; /* left arrow */ else ch = ((bShiftOn) ? 'S' : 's');}
break;
case 25: if (bFunc) ch = 130; /* right arrow */ else ch = ((bShiftOn) ? 'D' : 'd'); break;
case 26: if (bFunc) ch = '{'; else ch = ((bShiftOn) ? 'F' : 'f'); break;
case 27: if (bFunc) ch = '}'; else ch = ((bShiftOn) ? 'G' : 'g'); break;
case 28: if (bCtrl) ch = 8; /* CTRL-H or BACKSPACE */ else ch = ((bShiftOn) ? 'H' : 'h'); break;
case 29: ch = ((bShiftOn) ? 'J' : 'j'); break;
case 30: ch = ((bShiftOn) ? 'K' : 'k'); break;
case 31: ch = ((bShiftOn) ? 'L' : 'l'); break;
case 32: ch = ((bShiftOn) ? ':' : ';'); break;
case 33: ch = '\n'; break;
case 35: if (bCtrl) ch = 26; /* CTRL-Z */
else {if (bFunc) ch = '\\'; else ch = ((bShiftOn) ? 'Z' : 'z');}
break;
case 36: if (bFunc) ch = 131; /* down arrow */ else ch = ((bShiftOn) ? 'X' : 'x'); break;
case 37: if (bCtrl) ch = 3; /* CTRL-C */
else { if (bFunc) ch = '`'; else ch = ((bShiftOn) ? 'C' : 'c');}
break;
case 38: ch = ((bShiftOn) ? 'V' : 'v'); break;
case 39: ch = ((bShiftOn) ? 'B' : 'b'); break;
case 40: ch = ((bShiftOn) ? 'N' : 'n'); break;
case 41: ch = ((bShiftOn) ? 'M' : 'm'); break;
case 42: ch = ((bShiftOn) ? '<' : ','); break;
case 43: ch = ((bShiftOn) ? '>' : '.'); break;
case 44: ch = 0; break;
case 45: ch = 0; break;
case 46: ch = 0; break;
case 47: ch = ' '; break;
case 48: ch = 0; break;
default: ch = 0; break;
}
bShiftOn = 0; bLock = 0; bFunc = 0; bCtrl = 0;
return ch;
}
/*
* -------------- MAIN LOOP ---------------------
*/
main()
{
BYTE kcodehex[5];
BYTE kcodedec[10];
BYTE rowbuf[3];
BYTE row=0;
BYTE ch=0;
init_vars();
init_ports();
lcd_init();
delay(10);
// wait for I2C bus to become available
while (I2C_LineBusy())
{
lcdmsg(dis3, 4, 255); // Line busy message to LCD
}
lcd_text(0,dis1);
lcd_text(0x40,dis2);
delay(255);
delay(255);
while(1)
{
if((row=read_kb())!=0)
{
_uitoa(keycode,kcodehex,16);
_uitoa(keycode,kcodedec,10);
_uitoa(row,rowbuf,10);
strcpy(dispbuf,kcodehex);
strcat(dispbuf,":");
strcat(dispbuf,kcodedec);
strcat(dispbuf,":");
strcat(dispbuf,rowbuf);
lcd_text(0x40,dispbuf);
ch = convKeyCode2Char(keycode);
if (ch != 0)
I2C_SendKey(ch);
}
else
{
lcd_clear(0x40);
}
}
}
and here is the Arduino code:
// Wire Slave Receiver
// by Nicholas Zambetti <http://www.zambetti.com>
// Demonstrates use of the Wire library
// Receives data as an I2C/TWI slave device
// Refer to the "Wire Master Writer" example for use with this
// Created 29 March 2006
// This example code is in the public domain.
#include <Wire.h>
boolean bShiftPressed = false;
void setup()
{
Wire.begin(4); // join i2c bus with address #4
Wire.onReceive(receiveEvent); // register event
Serial.begin(9600); // start serial for output
Serial.println("I2C Slave Receiver");
}
void loop()
{
delay(100);
}
char getCharFromKeyCode(int c)
{
char ch = 0;
switch (c)
{
case 128: break;
case 129: break;
case 130: break;
case 131: break;
default: ch = c; break;
}
return ch;
}
// function that executes whenever data is received from master
// this function is registered as an event, see setup()
void receiveEvent(int howMany)
{
char ch;
int c;
while(1 < Wire.available()) // loop through all but the last
{
c = Wire.read(); // receive byte as an integer
ch = getCharFromKeyCode(c);
if (ch)
Serial.print(ch); // print the character
}
c = Wire.read(); // receive byte as an integer
ch = getCharFromKeyCode(c);
if (ch)
Serial.print(ch); // print the character
}
Some pictures of the prototype and a screenshot of the Arduino's serial console on the receiving end:
Thank you for looking.
Marek Karcz
12/13/2012