Months of testing with various Arduino
projects has proven that the I2C keyboard prototype is working therefore I
decided to assemble it on the permanent board. I used a RadioShack's
pre-drilled stripboard number 276-168B and point to point soldering
technique with 30-gauge solderable magnet wire (insulation coating
dissolves under heat from soldering iron). I have made modifications
to the original circuit diagram. I added programming connector so the
firmware can be updated without taking out the AT89S52 chip out of
the board. All you need is an ISP programmer with 10 pin connector
and you need to open the J4 jumper pins 1, 2 for programming mode. A
fact worth noting is that I connected AT89S52's pin #31 (/EA) to VCC
- a must for an internal program execution mode. It took me an extra
hour to figure this out when my board wasn't working. I apparently
had this connection done on the breadboard but I forgot to update it
on the diagram. Months passed since and I forgot. I also replaced
ugly keyboard and LCD symbols on the diagram with connector symbols.
The board is built in such a way that it can also serve as a basic
development board for AT89S52. It has standard 16x2 16-pin LCD
connector for HDD44780 compatible display which can be used as multi
purpose I/O connector in case LCD display is not used and 15-pin
connector for the matrix keyboard (TI-99/4A) can be used as a multi
purpose I/O ports connector as well. I may need to add some more I/O
connectors and a serial port in case I use this board for any other
purpose than I2C keyboard driver, but this is not likely. I will
rather build another one, but I digress.
I did changes to the firmware since last update:
/*
* Project: TI99-4A I2C keyboard.
* Module: ti994a
* Author: Marek Karcz
* Updated: 2012/12/30
* Purpose: TI99-4A keyboard and LCD disp. with micro connected to I2C.
* I2C input device. Works in MASTER mode.
* I2C output device (LCD). 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. - DONE
* 1a) Keyboard scan must run in interrupt and queue the keys in a buffer. - DONE
* 2) Cover all CTRL ANSI codes.
* 3) Receive characters from I2C bus and display on LCD.
* In progress...
* Current protocol:
* * Device sends a single key code to I2C bus.
* * Device requests single key code from I2C bus (echo).
* Planned expansion to protocol:
* * Device sends a single key code to I2C bus.
* * Device requests number of available bytes for LCD display from I2C bus.
* * Device reads all available bytes from I2C bus.
* 4) Fully implement arbitration and clock stretching. - DONE
* 5) Figure out why Arduino does not ACK when addressed as slave-receiver. - DONE
* (NOTE: implementation of #4 and polling data until ACK returns OK)
* 6) Use extra port line to generate interrupt for external receiver/CPU.
*
*/
#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
//#define _nop4_() __asm NOP NOP NOP NOP NOP NOP __endasm
#define _nop4_() __asm NOP NOP NOP 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
#define ACK FALSE
#define NACK TRUE
#define TRUE 1
#define FALSE 0
#define WAIT4ACK 100
#define KEYBUFLEN 16
#define INTDIVCT 0
#define KBRPTDEL 18 // keyboard repeat delay
BYTE code g_ucaDispMsgPrdNameVer[] = {"I2C kbrd 2.4 "};
BYTE code g_ucaDispMsg2[] = {"(C) Marek Karcz "};
BYTE code g_ucaDispMsgLineBusy[] = {"I2C line busy..."};
BYTE g_ucaDispBuf[17];
BYTE g_ucKeyCode=0;
BYTE g_ucPrevKey=0;
BYTE g_ucRow=0;
BYTE g_ucColumn=0;
BYTE code g_ucaKbMatrix[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 g_bShiftOn = FALSE;
BOOL g_bCtrl = FALSE;
BOOL g_bFunc = FALSE;
BOOL g_bLock = FALSE;
BOOL g_bStarted = FALSE;
BYTE g_ucaKeyBuf[KEYBUFLEN];
BYTE g_ucKeyBufStartIndex = 0;// at the beginning buffer is empty
BYTE g_ucKeyBufEndIndex = 0; // StartIndex == EndIndex -> buffer empty
BYTE g_ucDivIntCt = INTDIVCT; // kb scan interrupt divider/counter
BYTE g_ucKbRptDel = KBRPTDEL; // kb repeat delay counter
void init_SFR(void)
{
TMOD |= 0x01; // timer 0, mode 1, 16 bit
TH0 = 0x70; // for 40 ms timer (on XTAL=11.0592 MHz)
TL0 = 0x00; // keyboard scan frequency is 25/INTDIVCT/s
IE = 0xA2; // 0x82; // enable IE.1 (timer 0) and global interrupt. (IE.7)
TR0 = 1; // timer 0 runs
}
void init_ports(void)
{
KBP1 = 0x00;
KBP2 = 0xFF;
P1 = 0xFF;
SDA_IN = 1;
SCL_IN = 1;
SDA_OUT = 0;
SCL_OUT = 0;
}
void init_g_ucaDispBuf(void)
{
BYTE i = 0;
for(i=0; i<16; i++)
{
g_ucaDispBuf[i]=32;
}
g_ucaDispBuf[i]=0;
}
void init_vars(void)
{
BYTE i = 0;
init_g_ucaDispBuf();
g_ucKeyCode=0xFF;
g_ucPrevKey=0;
g_ucRow=0;
g_ucColumn=0;
g_ucKeyBufStartIndex = g_ucKeyBufEndIndex = 0;
for (i=0; i<KEYBUFLEN; i++)
{
g_ucaKeyBuf[i] = 0;
}
g_ucDivIntCt = INTDIVCT;
g_ucKbRptDel = KBRPTDEL;
}
void delay(BYTE ms)
{
BYTE i;
while(ms--)
{
for(i = 0; i< 250; i++)
{
_nop4_();
}
}
}
/*
* LCD 16x2 driver functions.
*/
BOOL lcd_bz()
{
BOOL result;
rs = 0;
rw = 1;
ep = 1;
_nop4_();
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;
_nop4_();
ep = 1;
_nop4_();
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;
_nop4_();
ep = 1;
_nop4_();
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++;
}
}
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, g_ucaDispMsgPrdNameVer);
}
/*
* 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;
g_ucKeyCode = 0;
for (i=0,pattern=0x01; pattern!=0; pattern<<=1,i++)
{
KBP2 = 0xFF;
KBP1 = ~pattern;
keyscan = KBP2;
if(keyscan!=0xFF)
{
init_g_ucaDispBuf();
for(j=1,col=0; j!=0; j<<=1,col++)
{
if (~keyscan & j)
{
if (g_ucaKbMatrix[i][col] == 44) g_bShiftOn = TRUE;
else if (g_ucaKbMatrix[i][col] == 45) { g_bShiftOn = TRUE; g_bLock = TRUE; }
else if (g_ucaKbMatrix[i][col] == 46) g_bCtrl = TRUE;
else if (g_ucaKbMatrix[i][col] == 48) g_bFunc = TRUE;
else { g_ucRow = i; ret = i+1; g_ucColumn = col; g_ucKeyCode = g_ucaKbMatrix[g_ucRow][g_ucColumn]; }
}
}
}
}
// key released, reset flags
if (ret == 0)
g_bShiftOn = g_bLock = g_bCtrl = g_bFunc = FALSE;
return ret;
}
/*
* Keyboard buffer is a ring/round-about FIFO register.
*/
static void add2KeyBuf(BYTE kc) using 1
{
g_ucaKeyBuf[g_ucKeyBufEndIndex++] = kc;
if (g_ucKeyBufEndIndex >= KEYBUFLEN)
{
g_ucKeyBufEndIndex = 0;
}
}
static BYTE getKeyFromBuf(void) using 1
{
BYTE kbk = 0;
if (g_ucKeyBufStartIndex != g_ucKeyBufEndIndex)
{
kbk = g_ucaKeyBuf[g_ucKeyBufStartIndex++];
if (g_ucKeyBufStartIndex >= KEYBUFLEN)
{
g_ucKeyBufStartIndex = 0;
}
}
return kbk;
}
/*
* Interrupt routine - timer 0.
* Scan keyboard, queue pressed keys in the buffer;
*/
void tim0(void) interrupt 1 using 1
{
TH0=0x70; // reload counter
TL0=0x00;
if (g_ucDivIntCt) g_ucDivIntCt--;
if (g_ucDivIntCt == 0)
{
if (read_kb())
{
if (g_ucKeyCode != g_ucPrevKey)
{
add2KeyBuf(g_ucKeyCode);
g_ucKbRptDel = KBRPTDEL;
}
else
{
if (g_ucKbRptDel) g_ucKbRptDel--;
if (g_ucKbRptDel == 0)
{
add2KeyBuf(g_ucKeyCode);
g_ucKbRptDel = KBRPTDEL/4;
}
}
}
g_ucPrevKey = g_ucKeyCode;
g_ucDivIntCt = INTDIVCT;
}
}
/*
* I2C bus communication driver functions.
*
* NOTE: in/out data and clk signals inverted
*
*/
BOOL I2C_SclBusy(void)
{
return (SCL_IN == I2C_LOW);
}
BOOL I2C_SdaBusy(void)
{
return (SDA_IN == I2C_LOW);
}
BOOL I2C_LineBusy(void)
{
return (I2C_SclBusy() || I2C_SdaBusy());
}
/*
* Arbitration of the I2C line.
* Returns TRUE if won, FALSE if lost.
*/
BOOL I2C_Arbitration(void)
{
return ((I2C_SdaBusy()) ? FALSE : TRUE);
}
void I2C_StretchScl(void)
{
int nTimeOut = 2000;
while (I2C_SclBusy() && nTimeOut > 0) // clock stretching
{
nTimeOut--; // prevent hang up
}
}
/* Send start condition with clock stretching
* and arbitration of I2C line.
*
*/
void I2C_Start(void)
{
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
if (g_bStarted) // already started - do restart
{
_nop4_();
I2C_StretchScl();
_nop4_();
}
if (I2C_Arbitration())
{
_nop4_();
SDA_OUT = I2C_LOW;
_nop4_();
SCL_OUT = I2C_LOW;
g_bStarted = TRUE;
}
}
void I2C_Stop(void) /*Stop condition*/
{
I2C_StretchScl();
if (I2C_Arbitration())
{
SDA_OUT = I2C_LOW;
SCL_OUT = I2C_LOW;
_nop4_();
SCL_OUT = I2C_HIGH;
_nop4_();
SDA_OUT = I2C_HIGH;
_nop4_();
g_bStarted = FALSE;
}
}
void I2C_WriteBit(BOOL bitval)
{
SDA_OUT = ((bitval) ? I2C_HIGH : I2C_LOW);
_nop4_();
SCL_OUT = I2C_HIGH;
I2C_StretchScl();
if (!bitval || I2C_Arbitration())
{
_nop4_();
SCL_OUT = I2C_LOW;
}
}
BOOL I2C_ReadBit(void)
{
BOOL br = FALSE;
SCL_OUT = I2C_HIGH;
_nop4_();
I2C_StretchScl();
br = ((SDA_IN == I2C_HIGH) ? TRUE : FALSE);
_nop4_();
SCL_OUT = I2C_LOW;
return br;
}
BOOL I2C_WriteByte(BOOL sendstart,
BOOL sendstop,
BYTE byteval)
{
BYTE bitc = 0;
BOOL nack = FALSE;
if (sendstart)
{
I2C_Start();
}
for (bitc=0; bitc<8; bitc++)
{
I2C_WriteBit((byteval & 0x80) != 0);
byteval <<= 1;
}
nack = I2C_ReadBit();
if (sendstop)
{
I2C_Stop();
}
return nack;
}
BYTE I2C_ReadByte(BOOL nack,
BOOL sendstop)
{
BYTE rbyte = 0;
BYTE bitc = 0;
for (bitc=0; bitc<8; bitc++)
{
rbyte = (rbyte << 1) | I2C_ReadBit();
}
I2C_WriteBit(nack);
if (sendstop)
{
I2C_Stop();
}
return rbyte;
}
void I2C_SendKey(BYTE kc)
{
BOOL nack = TRUE;
int nTimeOut = WAIT4ACK;
lcd_pos(0x4b); lcd_wdat('S');
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
_nop4_();
// slave receiver address = 4
// shift left for 7 bit addr. and set LSB (R/W bit)
// LSB = 0 - WRITE
while (nack && nTimeOut > 0)
{
nack = I2C_WriteByte(TRUE,FALSE,(4<<1)&0xfe);
lcd_pos(0x4f); lcd_wdat((nack) ? 'N' : 'A');
nTimeOut--;
delay(5);
}
nTimeOut = WAIT4ACK;
nack = TRUE;
while (nack && nTimeOut > 0)
{
nack = I2C_WriteByte(FALSE,TRUE,kc);
lcd_pos(0x4a); lcd_wdat((nack) ? 'N' : 'A');
nTimeOut--;
delay(5);
}
}
BYTE I2C_GetKey(void)
{
BYTE brd = 0;
BOOL nack = TRUE;
int nTimeOut = WAIT4ACK;
lcd_pos(0x4b); lcd_wdat('G');
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
_nop4_();
// slave transmitter address = 4
// shift left for 7 bit addr. and set LSB (R/W bit)
// LSB = 1 - READ
while (nack && nTimeOut > 0)
{
nack = I2C_WriteByte(TRUE,FALSE,(4<<1)|0x01);
lcd_pos(0x4e); lcd_wdat((nack) ? 'N' : 'A');
nTimeOut--;
delay(5);
}
brd = I2C_ReadByte(NACK, TRUE);
return brd;
}
/*
* Convert keyboard scan code to ANSI ASCII code.
* (more or less :-) )
*/
BYTE convKeyCode2Char(BYTE kc)
{
BYTE ch = 0;
switch (kc)
{
case 1: ch = ((g_bShiftOn) ? '!' : '1'); break;
case 2: ch = ((g_bShiftOn) ? '@' : '2'); break;
case 3: ch = ((g_bShiftOn) ? '#' : '3'); break;
case 4: ch = ((g_bShiftOn) ? '$' : '4'); break;
case 5: ch = ((g_bShiftOn) ? '%' : '5'); break;
case 6: ch = ((g_bShiftOn) ? '^' : '6'); break;
case 7: ch = ((g_bShiftOn) ? '&' : '7'); break;
case 8: ch = ((g_bShiftOn) ? '*' : '8'); break;
case 9: ch = ((g_bShiftOn) ? '(' : '9'); break;
case 10: ch = ((g_bShiftOn) ? ')' : '0'); break;
case 11: ch = ((g_bShiftOn) ? '+' : '='); break;
case 12: if (g_bCtrl) ch = 17; /* CTRL-Q */ else ch = ((g_bShiftOn) ? 'Q' : 'q'); break;
case 13: if (g_bFunc) ch = '~'; else ch = ((g_bShiftOn) ? 'W' : 'w'); break;
case 14: if (g_bFunc) ch = 128; /* up arrow */ else ch = ((g_bShiftOn) ? 'E' : 'e'); break;
case 15: if (g_bFunc) ch = '['; else ch = ((g_bShiftOn) ? 'R' : 'r'); break;
case 16: if (g_bFunc) ch = ']'; else ch = ((g_bShiftOn) ? 'T' : 't'); break;
case 17: ch = ((g_bShiftOn) ? 'Y' : 'y'); break;
case 18: if (g_bFunc) ch = '_'; else ch = ((g_bShiftOn) ? 'U' : 'u'); break;
case 19: if (g_bFunc) ch = '?'; else ch = ((g_bShiftOn) ? 'I' : 'i'); break;
case 20: if (g_bFunc) ch = '\''; else ch = ((g_bShiftOn) ? 'O' : 'o'); break;
case 21: if (g_bFunc) ch = '"'; else ch = ((g_bShiftOn) ? 'P' : 'p'); break;
case 22: ch = ((g_bShiftOn) ? '-' : '/'); break;
case 23: if (g_bFunc) ch = '|'; else ch = ((g_bShiftOn) ? 'A' : 'a'); break;
case 24: if (g_bCtrl) ch = 19; /* CTRL-S */
else {if (g_bFunc) ch = 129; /* left arrow */ else ch = ((g_bShiftOn) ? 'S' : 's');}
break;
case 25: if (g_bFunc) ch = 130; /* right arrow */ else ch = ((g_bShiftOn) ? 'D' : 'd'); break;
case 26: if (g_bFunc) ch = '{'; else ch = ((g_bShiftOn) ? 'F' : 'f'); break;
case 27: if (g_bFunc) ch = '}'; else ch = ((g_bShiftOn) ? 'G' : 'g'); break;
case 28: if (g_bCtrl) ch = 8; /* CTRL-H or BACKSPACE */ else ch = ((g_bShiftOn) ? 'H' : 'h'); break;
case 29: ch = ((g_bShiftOn) ? 'J' : 'j'); break;
case 30: ch = ((g_bShiftOn) ? 'K' : 'k'); break;
case 31: ch = ((g_bShiftOn) ? 'L' : 'l'); break;
case 32: ch = ((g_bShiftOn) ? ':' : ';'); break;
case 33: ch = '\n'; break;
case 35: if (g_bCtrl) ch = 26; /* CTRL-Z */
else {if (g_bFunc) ch = '\\'; else ch = ((g_bShiftOn) ? 'Z' : 'z');}
break;
case 36: if (g_bFunc) ch = 131; /* down arrow */ else ch = ((g_bShiftOn) ? 'X' : 'x'); break;
case 37: if (g_bCtrl) ch = 3; /* CTRL-C */
else { if (g_bFunc) ch = '`'; else ch = ((g_bShiftOn) ? 'C' : 'c');}
break;
case 38: ch = ((g_bShiftOn) ? 'V' : 'v'); break;
case 39: ch = ((g_bShiftOn) ? 'B' : 'b'); break;
case 40: ch = ((g_bShiftOn) ? 'N' : 'n'); break;
case 41: ch = ((g_bShiftOn) ? 'M' : 'm'); break;
case 42: ch = ((g_bShiftOn) ? '<' : ','); break;
case 43: ch = ((g_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;
}
g_bShiftOn = g_bLock = g_bFunc = g_bCtrl = FALSE;
return ch;
}
/*
* -------------- MAIN LOOP ---------------------
*/
main()
{
BYTE kcodehex[5];
BYTE kcodedec[10];
BYTE rowbuf[3];
BYTE ch = 0;
BYTE key = 0;
init_vars();
init_ports();
lcd_init();
init_SFR();
delay(10);
// wait for I2C bus to become available
while (I2C_LineBusy())
{
lcdmsg(g_ucaDispMsgLineBusy, 4, 255); // Line busy message to LCD
}
lcd_text(0,g_ucaDispMsgPrdNameVer);
lcd_text(0x40,g_ucaDispMsg2);
delay(255);
delay(255);
while(1)
{
if ((key = getKeyFromBuf()) != 0)
{
_uitoa(g_ucKeyCode,kcodehex,16);
_uitoa(g_ucKeyCode,kcodedec,10);
strcpy(g_ucaDispBuf,kcodehex);
strcat(g_ucaDispBuf,":");
strcat(g_ucaDispBuf,kcodedec);
lcd_text(0x40,g_ucaDispBuf);
ch = convKeyCode2Char(key);
if (ch != 0)
{
I2C_SendKey(ch);
delay(1);
ch = I2C_GetKey();
if (ch != 0)
{
rowbuf[0] = ch;
rowbuf[1] = 0;
lcd_text(0x0f, rowbuf);
}
}
}
else
{
lcd_clear(0x40);
}
}
}
LCD display is not really needed. I used it for debugging purposes. It can be safely taken out without any changes to the firmware, the controller will still work.
/*
* Project: TI99-4A I2C keyboard.
* Module: ti994a
* Author: Marek Karcz
* Updated: 2012/12/30
* Purpose: TI99-4A keyboard and LCD disp. with micro connected to I2C.
* I2C input device. Works in MASTER mode.
* I2C output device (LCD). 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. - DONE
* 1a) Keyboard scan must run in interrupt and queue the keys in a buffer. - DONE
* 2) Cover all CTRL ANSI codes.
* 3) Receive characters from I2C bus and display on LCD.
* In progress...
* Current protocol:
* * Device sends a single key code to I2C bus.
* * Device requests single key code from I2C bus (echo).
* Planned expansion to protocol:
* * Device sends a single key code to I2C bus.
* * Device requests number of available bytes for LCD display from I2C bus.
* * Device reads all available bytes from I2C bus.
* 4) Fully implement arbitration and clock stretching. - DONE
* 5) Figure out why Arduino does not ACK when addressed as slave-receiver. - DONE
* (NOTE: implementation of #4 and polling data until ACK returns OK)
* 6) Use extra port line to generate interrupt for external receiver/CPU.
*
*/
#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
//#define _nop4_() __asm NOP NOP NOP NOP NOP NOP __endasm
#define _nop4_() __asm NOP NOP NOP 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
#define ACK FALSE
#define NACK TRUE
#define TRUE 1
#define FALSE 0
#define WAIT4ACK 100
#define KEYBUFLEN 16
#define INTDIVCT 0
#define KBRPTDEL 18 // keyboard repeat delay
BYTE code g_ucaDispMsgPrdNameVer[] = {"I2C kbrd 2.4 "};
BYTE code g_ucaDispMsg2[] = {"(C) Marek Karcz "};
BYTE code g_ucaDispMsgLineBusy[] = {"I2C line busy..."};
BYTE g_ucaDispBuf[17];
BYTE g_ucKeyCode=0;
BYTE g_ucPrevKey=0;
BYTE g_ucRow=0;
BYTE g_ucColumn=0;
BYTE code g_ucaKbMatrix[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 g_bShiftOn = FALSE;
BOOL g_bCtrl = FALSE;
BOOL g_bFunc = FALSE;
BOOL g_bLock = FALSE;
BOOL g_bStarted = FALSE;
BYTE g_ucaKeyBuf[KEYBUFLEN];
BYTE g_ucKeyBufStartIndex = 0;// at the beginning buffer is empty
BYTE g_ucKeyBufEndIndex = 0; // StartIndex == EndIndex -> buffer empty
BYTE g_ucDivIntCt = INTDIVCT; // kb scan interrupt divider/counter
BYTE g_ucKbRptDel = KBRPTDEL; // kb repeat delay counter
void init_SFR(void)
{
TMOD |= 0x01; // timer 0, mode 1, 16 bit
TH0 = 0x70; // for 40 ms timer (on XTAL=11.0592 MHz)
TL0 = 0x00; // keyboard scan frequency is 25/INTDIVCT/s
IE = 0xA2; // 0x82; // enable IE.1 (timer 0) and global interrupt. (IE.7)
TR0 = 1; // timer 0 runs
}
void init_ports(void)
{
KBP1 = 0x00;
KBP2 = 0xFF;
P1 = 0xFF;
SDA_IN = 1;
SCL_IN = 1;
SDA_OUT = 0;
SCL_OUT = 0;
}
void init_g_ucaDispBuf(void)
{
BYTE i = 0;
for(i=0; i<16; i++)
{
g_ucaDispBuf[i]=32;
}
g_ucaDispBuf[i]=0;
}
void init_vars(void)
{
BYTE i = 0;
init_g_ucaDispBuf();
g_ucKeyCode=0xFF;
g_ucPrevKey=0;
g_ucRow=0;
g_ucColumn=0;
g_ucKeyBufStartIndex = g_ucKeyBufEndIndex = 0;
for (i=0; i<KEYBUFLEN; i++)
{
g_ucaKeyBuf[i] = 0;
}
g_ucDivIntCt = INTDIVCT;
g_ucKbRptDel = KBRPTDEL;
}
void delay(BYTE ms)
{
BYTE i;
while(ms--)
{
for(i = 0; i< 250; i++)
{
_nop4_();
}
}
}
/*
* LCD 16x2 driver functions.
*/
BOOL lcd_bz()
{
BOOL result;
rs = 0;
rw = 1;
ep = 1;
_nop4_();
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;
_nop4_();
ep = 1;
_nop4_();
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;
_nop4_();
ep = 1;
_nop4_();
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++;
}
}
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, g_ucaDispMsgPrdNameVer);
}
/*
* 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;
g_ucKeyCode = 0;
for (i=0,pattern=0x01; pattern!=0; pattern<<=1,i++)
{
KBP2 = 0xFF;
KBP1 = ~pattern;
keyscan = KBP2;
if(keyscan!=0xFF)
{
init_g_ucaDispBuf();
for(j=1,col=0; j!=0; j<<=1,col++)
{
if (~keyscan & j)
{
if (g_ucaKbMatrix[i][col] == 44) g_bShiftOn = TRUE;
else if (g_ucaKbMatrix[i][col] == 45) { g_bShiftOn = TRUE; g_bLock = TRUE; }
else if (g_ucaKbMatrix[i][col] == 46) g_bCtrl = TRUE;
else if (g_ucaKbMatrix[i][col] == 48) g_bFunc = TRUE;
else { g_ucRow = i; ret = i+1; g_ucColumn = col; g_ucKeyCode = g_ucaKbMatrix[g_ucRow][g_ucColumn]; }
}
}
}
}
// key released, reset flags
if (ret == 0)
g_bShiftOn = g_bLock = g_bCtrl = g_bFunc = FALSE;
return ret;
}
/*
* Keyboard buffer is a ring/round-about FIFO register.
*/
static void add2KeyBuf(BYTE kc) using 1
{
g_ucaKeyBuf[g_ucKeyBufEndIndex++] = kc;
if (g_ucKeyBufEndIndex >= KEYBUFLEN)
{
g_ucKeyBufEndIndex = 0;
}
}
static BYTE getKeyFromBuf(void) using 1
{
BYTE kbk = 0;
if (g_ucKeyBufStartIndex != g_ucKeyBufEndIndex)
{
kbk = g_ucaKeyBuf[g_ucKeyBufStartIndex++];
if (g_ucKeyBufStartIndex >= KEYBUFLEN)
{
g_ucKeyBufStartIndex = 0;
}
}
return kbk;
}
/*
* Interrupt routine - timer 0.
* Scan keyboard, queue pressed keys in the buffer;
*/
void tim0(void) interrupt 1 using 1
{
TH0=0x70; // reload counter
TL0=0x00;
if (g_ucDivIntCt) g_ucDivIntCt--;
if (g_ucDivIntCt == 0)
{
if (read_kb())
{
if (g_ucKeyCode != g_ucPrevKey)
{
add2KeyBuf(g_ucKeyCode);
g_ucKbRptDel = KBRPTDEL;
}
else
{
if (g_ucKbRptDel) g_ucKbRptDel--;
if (g_ucKbRptDel == 0)
{
add2KeyBuf(g_ucKeyCode);
g_ucKbRptDel = KBRPTDEL/4;
}
}
}
g_ucPrevKey = g_ucKeyCode;
g_ucDivIntCt = INTDIVCT;
}
}
/*
* I2C bus communication driver functions.
*
* NOTE: in/out data and clk signals inverted
*
*/
BOOL I2C_SclBusy(void)
{
return (SCL_IN == I2C_LOW);
}
BOOL I2C_SdaBusy(void)
{
return (SDA_IN == I2C_LOW);
}
BOOL I2C_LineBusy(void)
{
return (I2C_SclBusy() || I2C_SdaBusy());
}
/*
* Arbitration of the I2C line.
* Returns TRUE if won, FALSE if lost.
*/
BOOL I2C_Arbitration(void)
{
return ((I2C_SdaBusy()) ? FALSE : TRUE);
}
void I2C_StretchScl(void)
{
int nTimeOut = 2000;
while (I2C_SclBusy() && nTimeOut > 0) // clock stretching
{
nTimeOut--; // prevent hang up
}
}
/* Send start condition with clock stretching
* and arbitration of I2C line.
*
*/
void I2C_Start(void)
{
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
if (g_bStarted) // already started - do restart
{
_nop4_();
I2C_StretchScl();
_nop4_();
}
if (I2C_Arbitration())
{
_nop4_();
SDA_OUT = I2C_LOW;
_nop4_();
SCL_OUT = I2C_LOW;
g_bStarted = TRUE;
}
}
void I2C_Stop(void) /*Stop condition*/
{
I2C_StretchScl();
if (I2C_Arbitration())
{
SDA_OUT = I2C_LOW;
SCL_OUT = I2C_LOW;
_nop4_();
SCL_OUT = I2C_HIGH;
_nop4_();
SDA_OUT = I2C_HIGH;
_nop4_();
g_bStarted = FALSE;
}
}
void I2C_WriteBit(BOOL bitval)
{
SDA_OUT = ((bitval) ? I2C_HIGH : I2C_LOW);
_nop4_();
SCL_OUT = I2C_HIGH;
I2C_StretchScl();
if (!bitval || I2C_Arbitration())
{
_nop4_();
SCL_OUT = I2C_LOW;
}
}
BOOL I2C_ReadBit(void)
{
BOOL br = FALSE;
SCL_OUT = I2C_HIGH;
_nop4_();
I2C_StretchScl();
br = ((SDA_IN == I2C_HIGH) ? TRUE : FALSE);
_nop4_();
SCL_OUT = I2C_LOW;
return br;
}
BOOL I2C_WriteByte(BOOL sendstart,
BOOL sendstop,
BYTE byteval)
{
BYTE bitc = 0;
BOOL nack = FALSE;
if (sendstart)
{
I2C_Start();
}
for (bitc=0; bitc<8; bitc++)
{
I2C_WriteBit((byteval & 0x80) != 0);
byteval <<= 1;
}
nack = I2C_ReadBit();
if (sendstop)
{
I2C_Stop();
}
return nack;
}
BYTE I2C_ReadByte(BOOL nack,
BOOL sendstop)
{
BYTE rbyte = 0;
BYTE bitc = 0;
for (bitc=0; bitc<8; bitc++)
{
rbyte = (rbyte << 1) | I2C_ReadBit();
}
I2C_WriteBit(nack);
if (sendstop)
{
I2C_Stop();
}
return rbyte;
}
void I2C_SendKey(BYTE kc)
{
BOOL nack = TRUE;
int nTimeOut = WAIT4ACK;
lcd_pos(0x4b); lcd_wdat('S');
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
_nop4_();
// slave receiver address = 4
// shift left for 7 bit addr. and set LSB (R/W bit)
// LSB = 0 - WRITE
while (nack && nTimeOut > 0)
{
nack = I2C_WriteByte(TRUE,FALSE,(4<<1)&0xfe);
lcd_pos(0x4f); lcd_wdat((nack) ? 'N' : 'A');
nTimeOut--;
delay(5);
}
nTimeOut = WAIT4ACK;
nack = TRUE;
while (nack && nTimeOut > 0)
{
nack = I2C_WriteByte(FALSE,TRUE,kc);
lcd_pos(0x4a); lcd_wdat((nack) ? 'N' : 'A');
nTimeOut--;
delay(5);
}
}
BYTE I2C_GetKey(void)
{
BYTE brd = 0;
BOOL nack = TRUE;
int nTimeOut = WAIT4ACK;
lcd_pos(0x4b); lcd_wdat('G');
SDA_OUT = I2C_HIGH;
SCL_OUT = I2C_HIGH;
_nop4_();
// slave transmitter address = 4
// shift left for 7 bit addr. and set LSB (R/W bit)
// LSB = 1 - READ
while (nack && nTimeOut > 0)
{
nack = I2C_WriteByte(TRUE,FALSE,(4<<1)|0x01);
lcd_pos(0x4e); lcd_wdat((nack) ? 'N' : 'A');
nTimeOut--;
delay(5);
}
brd = I2C_ReadByte(NACK, TRUE);
return brd;
}
/*
* Convert keyboard scan code to ANSI ASCII code.
* (more or less :-) )
*/
BYTE convKeyCode2Char(BYTE kc)
{
BYTE ch = 0;
switch (kc)
{
case 1: ch = ((g_bShiftOn) ? '!' : '1'); break;
case 2: ch = ((g_bShiftOn) ? '@' : '2'); break;
case 3: ch = ((g_bShiftOn) ? '#' : '3'); break;
case 4: ch = ((g_bShiftOn) ? '$' : '4'); break;
case 5: ch = ((g_bShiftOn) ? '%' : '5'); break;
case 6: ch = ((g_bShiftOn) ? '^' : '6'); break;
case 7: ch = ((g_bShiftOn) ? '&' : '7'); break;
case 8: ch = ((g_bShiftOn) ? '*' : '8'); break;
case 9: ch = ((g_bShiftOn) ? '(' : '9'); break;
case 10: ch = ((g_bShiftOn) ? ')' : '0'); break;
case 11: ch = ((g_bShiftOn) ? '+' : '='); break;
case 12: if (g_bCtrl) ch = 17; /* CTRL-Q */ else ch = ((g_bShiftOn) ? 'Q' : 'q'); break;
case 13: if (g_bFunc) ch = '~'; else ch = ((g_bShiftOn) ? 'W' : 'w'); break;
case 14: if (g_bFunc) ch = 128; /* up arrow */ else ch = ((g_bShiftOn) ? 'E' : 'e'); break;
case 15: if (g_bFunc) ch = '['; else ch = ((g_bShiftOn) ? 'R' : 'r'); break;
case 16: if (g_bFunc) ch = ']'; else ch = ((g_bShiftOn) ? 'T' : 't'); break;
case 17: ch = ((g_bShiftOn) ? 'Y' : 'y'); break;
case 18: if (g_bFunc) ch = '_'; else ch = ((g_bShiftOn) ? 'U' : 'u'); break;
case 19: if (g_bFunc) ch = '?'; else ch = ((g_bShiftOn) ? 'I' : 'i'); break;
case 20: if (g_bFunc) ch = '\''; else ch = ((g_bShiftOn) ? 'O' : 'o'); break;
case 21: if (g_bFunc) ch = '"'; else ch = ((g_bShiftOn) ? 'P' : 'p'); break;
case 22: ch = ((g_bShiftOn) ? '-' : '/'); break;
case 23: if (g_bFunc) ch = '|'; else ch = ((g_bShiftOn) ? 'A' : 'a'); break;
case 24: if (g_bCtrl) ch = 19; /* CTRL-S */
else {if (g_bFunc) ch = 129; /* left arrow */ else ch = ((g_bShiftOn) ? 'S' : 's');}
break;
case 25: if (g_bFunc) ch = 130; /* right arrow */ else ch = ((g_bShiftOn) ? 'D' : 'd'); break;
case 26: if (g_bFunc) ch = '{'; else ch = ((g_bShiftOn) ? 'F' : 'f'); break;
case 27: if (g_bFunc) ch = '}'; else ch = ((g_bShiftOn) ? 'G' : 'g'); break;
case 28: if (g_bCtrl) ch = 8; /* CTRL-H or BACKSPACE */ else ch = ((g_bShiftOn) ? 'H' : 'h'); break;
case 29: ch = ((g_bShiftOn) ? 'J' : 'j'); break;
case 30: ch = ((g_bShiftOn) ? 'K' : 'k'); break;
case 31: ch = ((g_bShiftOn) ? 'L' : 'l'); break;
case 32: ch = ((g_bShiftOn) ? ':' : ';'); break;
case 33: ch = '\n'; break;
case 35: if (g_bCtrl) ch = 26; /* CTRL-Z */
else {if (g_bFunc) ch = '\\'; else ch = ((g_bShiftOn) ? 'Z' : 'z');}
break;
case 36: if (g_bFunc) ch = 131; /* down arrow */ else ch = ((g_bShiftOn) ? 'X' : 'x'); break;
case 37: if (g_bCtrl) ch = 3; /* CTRL-C */
else { if (g_bFunc) ch = '`'; else ch = ((g_bShiftOn) ? 'C' : 'c');}
break;
case 38: ch = ((g_bShiftOn) ? 'V' : 'v'); break;
case 39: ch = ((g_bShiftOn) ? 'B' : 'b'); break;
case 40: ch = ((g_bShiftOn) ? 'N' : 'n'); break;
case 41: ch = ((g_bShiftOn) ? 'M' : 'm'); break;
case 42: ch = ((g_bShiftOn) ? '<' : ','); break;
case 43: ch = ((g_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;
}
g_bShiftOn = g_bLock = g_bFunc = g_bCtrl = FALSE;
return ch;
}
/*
* -------------- MAIN LOOP ---------------------
*/
main()
{
BYTE kcodehex[5];
BYTE kcodedec[10];
BYTE rowbuf[3];
BYTE ch = 0;
BYTE key = 0;
init_vars();
init_ports();
lcd_init();
init_SFR();
delay(10);
// wait for I2C bus to become available
while (I2C_LineBusy())
{
lcdmsg(g_ucaDispMsgLineBusy, 4, 255); // Line busy message to LCD
}
lcd_text(0,g_ucaDispMsgPrdNameVer);
lcd_text(0x40,g_ucaDispMsg2);
delay(255);
delay(255);
while(1)
{
if ((key = getKeyFromBuf()) != 0)
{
_uitoa(g_ucKeyCode,kcodehex,16);
_uitoa(g_ucKeyCode,kcodedec,10);
strcpy(g_ucaDispBuf,kcodehex);
strcat(g_ucaDispBuf,":");
strcat(g_ucaDispBuf,kcodedec);
lcd_text(0x40,g_ucaDispBuf);
ch = convKeyCode2Char(key);
if (ch != 0)
{
I2C_SendKey(ch);
delay(1);
ch = I2C_GetKey();
if (ch != 0)
{
rowbuf[0] = ch;
rowbuf[1] = 0;
lcd_text(0x0f, rowbuf);
}
}
}
else
{
lcd_clear(0x40);
}
}
}
LCD display is not really needed. I used it for debugging purposes. It can be safely taken out without any changes to the firmware, the controller will still work.
Arduino
test sketch code:
//
Wire Slave Receiver/Sender
//
by Marek Karcz 2012
//
based on examples
//
by Nicholas Zambetti <http://www.zambetti.com>
//
Demonstrates use of the Wire library
//
Receives data as an I2C/TWI slave device
//
Sends 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>
unsigned
char nLastRcvCh = 0;
void
setup()
{
Wire.begin(4);
// join i2c bus with address #4
Wire.onReceive(receiveEvent);
// register receive event
Wire.onRequest(requestEvent);
// register request event
Serial.begin(9600);
// start serial for output
Serial.println("I2C
Slave Receiver/Sender");
}
void
loop()
{
delay(100);
}
unsigned
char getCharFromKeyCode(unsigned char c)
{
unsigned
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;
unsigned
char c;
//Serial.print("receiveEvent:");
while(Wire.available())
// loop through all
{
c
= Wire.read(); // receive byte as an integer
ch
= getCharFromKeyCode(c);
if
(ch)
Serial.print(ch);
// print the character
}
nLastRcvCh
= c;
}
//
function that executes whenever data is requested by master
//
this function is registered as an event, see setup()
void
requestEvent()
{
//Serial.println(":requestEvent");
Wire.write(nLastRcvCh);
nLastRcvCh
= 0;
}
For the practical applications, I recommend to create 2 separate character buffers (one for I2C keyboard protocol and one for application use) for received via I2C bus characters and add them to the buffers as they come in receiveEvent method. Do not perform any more actions in the receiveEvent/requestEvent methods other than adding the characters to the buffer and sending back an echo in requestEvent as required by the keyboard driver protocol. In the main application loop, retrieve characters from the buffer designed for the application use. This will ensure lag free operation of the keyboard and will prevent locking up.
Here is an updated circuit diagram and
some pictures of the populated board and pictures of the board with
LCD display and keyboard attached and connected to the Arduino board
for testing. To date it is the cleanest point to point soldering
board I have ever made. I guess I am getting better at this, which is
good. I need all the training I can get before I start assembling my
MKHBC-8-R1 computer boards.
Thanks for looking at my blog. I hope you find it helpful.
Marek Karcz
05/27/2013