////////////////////////////////////////////////////////////////////////////////////////////////// // // I foolishly purchased a Lexitron word processor. I didn't realise that it would // take control of my life ..... // // The keyboard uC had died and without a keyboard the word processor is junk. There is very // little info available on the web so i figured my chances of finding a replacement were zip. // I also thought that if i did manage to find an unprogrammed replacement i would // struggle to find a development environment. Oh and there's the skill required to pack what's // required into a very old uC. // // An arduino replacement seemed more viable but still a lengthy reverse engineering and re-engineering // task. But ..... it's forced me to re-learn some C, play with some Arduino, and generally // get my head into some problem solving. // // Graham Lees // // UPDATE 15/1/25 // // I stumbled on a couple of VT202 that had working keyboards. They are very similar to the // VT1303. I was able to instrument the keyboard cable with a logic analyser. That has allowed me // see each scan code and to also get the timing for the various signals. // // It's worth noting that the microcontroller does not do everything. The Status register is read // without intervention, the strobe is generated with the help of a little external logic. // // Also worth noting is that the teensy is a Teensy++ 2.0 which is important to know when using the // arduino IDE. // // There are significant code changes required. Characters are sent to the 8085 on press and // on release of a key (except for shift). Autorepeat never originated from the keyboard. Spec Shift, // Margin Select, and Block perform tricks only appear to perform tricks - it's actually the software // that makes it all happen. The timing of the Strobe is very different from what i had come up with // originally. // // /////////////////////////////////////////////////////////////////////////////////////////////////// // Do the pinout const int Port_A7 = 35; const int Port_A6 = 34; const int Port_A5 = 33; const int Port_A4 = 32; const int Port_A3 = 31; const int Port_A2 = 30; const int Port_A1 = 29; const int Port_A0 = 28; const int Port_B7 = 27; const int Port_B6 = 26; const int Port_B5 = 25; const int Port_B4 = 24; const int Port_B3 = 23; const int Port_B2 = 22; const int Port_B1 = 21; const int Port_B0 = 20; const int Port_C7 = 17; const int Port_C6 = 16; const int Port_C5 = 15; const int Port_C4 = 14; const int Port_C3 = 13; const int Port_C2 = 12; const int Port_C1 = 11; const int Port_C0 = 10; const int Port_D7 = 7; const int Port_D6 = 6; const int Port_D5 = 5; const int Port_D4 = 4; const int Port_D3 = 3; const int Port_D2 = 2; const int Port_D1 = 1; const int Port_D0 = 0; const int Port_E7 = 19; const int Port_E6 = 18; const int Port_E5 = 0; const int Port_E4 = 0; const int Port_E3 = 0; const int Port_E2 = 0; const int Port_E1 = 9; const int Port_E0 = 8; const int Port_F7 = 45; const int Port_F6 = 44; const int Port_F5 = 43; const int Port_F4 = 42; const int Port_F3 = 41; const int Port_F2 = 40; const int Port_F1 = 39; const int Port_F0 = 38; /////////////////////////////////////////////////////////////////////////////////////////////// // I need to make the Teensy++ 2.0 map as easily as posible to the D8748 socket. // // Port_ Port_C aligns well with the D8748 Port_ DB, but after that it gets a bit awkward because // of the strange P1/P2 arrangement on the D8748. // // Port_B[7..0] can cover P1[7..0] - Drives the keyboard scanner: row, column, pulse // Port_C[7..0] can cover Port_B[7..0] - Bidirectional port for comms with uP // Port_D[3..0] can cover P2[7..4] - Control lines such as read for the 74LS374 // Port_E[7..0] can pick up loose ends like the uC input T1 which is used to sense a pressed key. // Port_F[6..3] can cover P2[3..0] - Drives the LEDs - may other things - not sure. // // On the original Cortron keyboard this is done by a microcontroller (D8748) but that seems to have lost the will to live. // // The Cortron uses the D8748 bidirectional Port, DB[7..0], to send the keystrokes to the uP. // // The uP can write commands back to the keyboard via this port. The byte is written into a 74LS374 which can be read from the uC. // When writing, the uC disables the 374 (U7) which is controlled by the P2[5] by negating the output enable ie take it high. // // After setting the character up on DB, the Lexitron uP is alerted by toggling either P2[7] or P2[6]. // They are ORed so they have to both be low before at least one is taken high. These two signals // also control the sound from the keyboard, making for an interesting sequencing challenge. // //////////////////////////////////////////////////////////////////////////////////////// const int uC_New_Command_L = Port_D4; //P2_4 Pin 6 on uC const int uC_RD_L = Port_D3; //P2_5 pin 36 on uC const int uC_GotOne_Loud_L = Port_D2; //P2_6 pin 37 on uC const int uC_GotOne_Adj_L = Port_D1; //P2_7 pin 38 on uC // I thought they had this setup to make the keys beep when pressed but i think // they ran out of output pins so needed to get three functions from two pins. // // Truth Table // // D2 D1 Strobe Adj_Tone Loud_Tone // ------------------------------------------- // 0 0 1 0 0 // 0 1 0 0 1 // 1 0 0 1 0 // 1 1 0 0 0 const int uC_LED0 = Port_F3; const int uC_LED1 = Port_F4; const int uC_LED2 = Port_F5; const int uC_LED3 = Port_F6; const int LED = Port_D6; const int uC_Sense = Port_E7; byte ch; byte ScanCode; byte lights; byte rx_command, rx_command2, rx_command_last; byte LastSendRelease; int i, j; int row, column; int KeyPressed[128]; int ShiftStateWhenKeyPressed[128]; int SpecShiftActive; int MarginSetActive; int AutoRepeat; void setup() { // Setup Serial Monitor Serial.begin(9600); // the baud rate does nothing because this is over USB. Serial.setTimeout(0xFFFF); // Setup Lexitron Processor I/O Port. Input=0. Output=255. DDRA = 0; DDRB = 0; DDRC = 255; DDRD = 0; DDRE = 0; DDRF = 255; // Set up some particular pins. pinMode(uC_RD_L, OUTPUT); pinMode(uC_GotOne_Loud_L, OUTPUT); pinMode(uC_GotOne_Adj_L, OUTPUT); pinMode(uC_Sense, INPUT); pinMode(uC_New_Command_L,INPUT); rx_command = 0; // We're only going to be sending characters to the word processor so we need to // turn off the 74LS374 to avoid contention. // Might be better to change this to a port write and retest. PORTD = 0b00001110; digitalWrite(uC_RD_L, HIGH); PORTC = 0; digitalWrite(uC_GotOne_Loud_L, HIGH); digitalWrite(uC_GotOne_Adj_L, HIGH); digitalWrite(LED, LOW); // Pointless Light Show for (lights = 0; lights < 17; lights++) { delay(100); PORTF = (lights << 3) & 0x78; } // Initialise the key status for each scan code for (ScanCode = 0; ScanCode < 128; ScanCode++) { KeyPressed[ScanCode] = 0; ShiftStateWhenKeyPressed[ScanCode] = 0; } // Set up for physical keyboard scan DDRB = 255; //set all bits to outputs. ?????? Move up the top and retest PORTB = 8; //set Bit3 high. ??? Remove and retest ScanCode = 0; ch = 0; PORTA = 1; //Not sure why i did this. // None of these are required. Remove and retest. SpecShiftActive = 0; MarginSetActive = 0; LastSendRelease = 0; AutoRepeat = 0; // Will do one row/column combination on each loop starting at 0/0 row = 0; column = 0; } // setup //This map comes out of a spreadsheet. const byte map_plain[80] = {80,16,48,57,32,64,99,73,36,67,83,19,52,51,4,35,101,68,37,100,85,84,54,53,6,5,34,70,66,102,95,86,0,50,8,2,0,40,79,98,111,90,60,62,7,11,106,46,75,103,15,26,0,3,1,22,107,69,41,110,63,91,25,108,33,43,109,44,76,59,47,89,58,55,9,10,96,71,74,105 }; // Kind of silly because it just adds 128 to the numbers above. const byte map_shift[80] = {208,144,176,185,160,192,227,201,164,195,211,147,180,179,132,163,229,196,165,228,213,212,182,181,134,133,162,198,194,230,223,214,0,178,136,130,0,168,207,226,239,218,188,190,135,139,234,174,203,231,143,250,0,131,129,150,235,197,169,238,191,219,153,236,161,171,237,172,204,187,175,217,186,183,137,138,224,199,202,233 }; const byte map_ascii[128]= {0,0,0,0,0,59,0,0,8,0,0,0,0,80,0,0,0,0,0,0,0,0,0,0,108,0,0,0,0,0,0,0,80,226,190,230,229,228,195,62,233,234,224,235,90,103,26,91,106,98,34,102,101,100,99,67,96,105,188,60,0,107,0,219,162,178,147,212,181,133,180,179,176,137,185,186,183,217,144,138,135,130,132,182,163,192,211,134,213,160,214,139,0,11,0,231,0,50,19,84,53,5,52,51,48,9,57,58,55,89,16,10,7,2,4,54,35,64,83,6,85,32,86,0,227,0,0,22, }; // A few keys get special treatment. The shift keys result in different characters being sent (usually). // The special shift key generates a code on being pressed and another code on release. The Lexitron // software works out what it means. const byte shiftleft = 50; const byte shiftright = 82; const byte shiftspec = 98; // Probably don't need to use this. const byte marginset = 112; // Probably don't need to use this. const byte zero = 0; byte map_index; int x, y; int index; int cur; char buffer[4]; char buffer2[80]; int Detected; int Detections; elapsedMillis DownTime; ///////////////////////////////////////////////////////////////////////////////////// // // D. sendch() function that sends a character to the uP // // // //////////////////////////////////////////////////////////////////////////////////////// void sendch(byte ch, byte press, byte autorepeat) { // Need some speed here so have to write direct to the ports PORTB = 8; //set Bit3 high. Why oh why????? Because this indicates a key down. // could remove the above line and retest PORTC = ch; delayMicroseconds(10); PORTD = 0b00001000; //This leaves uC_RD_L deactivated and activates the strobe. //A bit ugly. Could do in a more robust way. if (press==1) { if (autorepeat) { PORTB = 12; // This is what the real keybaord does but it doesn't seem to be important. } else { PORTB = 8; } delayMicroseconds(5); // 5us for press } else { PORTB = 0; delayMicroseconds(10); // 10us for release } PORTD = 0b00001110; //Deactivate the strobe delayMicroseconds(2000); // a short delay required for back to back characters from terminal input ?? //delay(100); //Serial.print("Kbd:"); //Serial.println(ch); } void loop() { ////////////////////////////////////////////////////////////////////// // // A. Receive Command from uProcessor // // When a new command is written to the command register uC_New_Command is asserted and the // 8085 is unable to write again until the data is read by the uC. // // //////////////////////////////////////////////////////////////////// //Define the command word bits const byte command_Click = 1; const byte command_setLED_TypeThru = 2; //Would need to connect a printer to check. const byte command_setLED_Select = 4; const byte command_setLED_ContType = 8; const byte command_setLEDs = 16; const byte command_Acknowledge = 32; const byte command_Bell = 64; //const byte command_setLED_TBD6 = 128; //Define the LED bits const byte LED_Select = Port_F4; const byte LED_ContType = Port_F3; const byte LED_TypeThru = Port_F6; //const byte LED_TBD1 = Port_F5; if (digitalRead(uC_New_Command_L) == LOW) { // Turn the data port around to be an input DDRC = 0; // Enable the 374 outputs digitalWrite(uC_RD_L, LOW); delayMicroseconds(10); //could be shorter than 10 // Read the command from the command register rx_command = PINC; // Turn off the 374 output -- this should reset the edge generator for the register // and clear uC_New_Command digitalWrite(uC_RD_L, HIGH); // Set the port back to an output DDRC = 255; //set all bits back to outputs //Serial.print("Rx: "); //Serial.println(rx_command); //so you can see the received word //Serial.println(LastSendRelease); if ((rx_command & command_setLEDs) > 0) { if ((rx_command & command_setLED_Select) > 0) { //Serial.println("LED Select High"); digitalWrite(LED_Select, HIGH); } else { //Serial.println("LED Select Low"); digitalWrite(LED_Select, LOW); } if ((rx_command & command_setLED_ContType) > 0) { //Serial.println("LED ContType High"); digitalWrite(LED_ContType, HIGH); } else { //Serial.println("LED ContType Low"); digitalWrite(LED_ContType, LOW); } if ((rx_command & command_setLED_TypeThru) > 0) { //Serial.println("LED ContType High"); digitalWrite(LED_TypeThru, HIGH); } else { //Serial.println("LED ContType Low"); digitalWrite(LED_TypeThru, LOW); } } //if if ((rx_command & command_Click) > 0) { //Serial.println("Click"); PORTD = 0b00001010; delay(1); PORTD = 0b00001110; } if ((rx_command & command_Acknowledge) > 0) { //Serial.println("Ack"); } else { //Serial.println("NoAck"); } // Do nothing as far as i can work out. Perhaps it should suspend kbd scanning. if ((rx_command & command_Bell) > 0) { PORTD = 0b00001100; delay(1); PORTD = 0b00001110; } } /////////////////////////////////////////////////////////////////////////// // // B. Check for terminal monitor input // // This allows a byte to be entered at the serial monitor and sends it // to the uP as if it resulted from a keystroke. This is for reverse engineering. // /////////////////////////////////////////////////////////////////////////// // Wait for character from the serial Port_ monitor if (Serial.available()) { #define Num 0 if (Num) { while (Serial.available()) { index = Serial.readBytesUntil('\n', buffer, 4); //newline or max of 3 chars buffer[index] = '\0'; //Serial.println(buffer); //so you can see the captured String ch = atoi(buffer); //convert readString into a number //Serial.print("Term:"); //so you can see the byte //Serial.println(ch); //so you can see the byte buffer[0] = '\0'; } //while //sendch(ch,1,0); delay(10); //sendch(ch,0,0); } else { while (Serial.available()) { index = Serial.readBytesUntil('\n', buffer2, 80); //newline or max of 80 chars Serial.write(19); buffer2[index] = '\n'; //Serial.write(10); //Serial.write(13); //Serial.print("=> "); //Serial.println(buffer2); //so you can see the captured String //Serial.print(" <="); //Serial.write(10); //Serial.write(13); for (int cur=0; cur 5) { Detected = 1; } //if } // while (dwell) if (Detected == 1 ) { // if the key is down then there are three cases: // 1. It's a shift key so just note that it's down. Nothing needs to be sent. // 2. The key down on the last scan so there's nothing to do. // 3. The key was up on the last scan so we should transmit the key press. In this case // the key code to be transmitted should have the MSB set if a shift key is down. if ((ScanCode != shiftleft) && (ScanCode != shiftright)) { if (KeyPressed[ScanCode] == 0) { Serial.print("Scan>"); //so you can see the byte Serial.println(ScanCode); //so you can see the byte //translate key to map index map_index = 10 * (ScanCode >> 4) + (ScanCode & 15); Serial.print("MI:"); //so you can see the byte Serial.println(map_index); //so you can see the byte if ((KeyPressed[shiftleft] > 0) || (KeyPressed[shiftright] > 0)) { ch = map_shift[map_index]; } else { ch = map_plain[map_index]; } //if AutoRepeat = 0; DownTime = 0; sendch(ch,1,0); } // new keypress else { //already down if ((KeyPressed[shiftleft] > 0) || (KeyPressed[shiftright] > 0)) { ch = map_shift[map_index]; } else { ch = map_plain[map_index]; } //if // Getting the limited autorepeat in the word processor working was effort i should not have expended! if ((DownTime > 300) ) { //this gets in before the cp/m autorepeat // This is what the oscilloscope showed me when i looked at a real keyboard ... // Never would have guessed it. if ((AutoRepeat == 0) && ((ch == 80) || (ch == 110) || (ch == 103) || (ch == 231) || (ch == 26) )) { if (ch == 80) { ch = 112; } else if (ch == 110) { ch = 126; } else if (ch == 103) { ch = 119; } else if (ch == 231) { ch = 247; } else if (ch == 26) { ch = 122; } sendch(ch,1,1); AutoRepeat = 1; // This is only required once. After that the word processor software does it. } } } } // if key other than shift KeyPressed[ScanCode] = 1; // Note that the key was down for all keys including shift ShiftStateWhenKeyPressed[ScanCode] = (ScanCode == shiftleft || ScanCode == shiftright); } // if key down detected else { // key down not detected ie key is up. // if the key is up then there are three cases: // 1. It's a shift key so just note that it's released. Nothing needs to be sent. // 2. The key was up on the last scan so there's nothing to do. // 3. The key was down on the last scan so we should transmit the key release. In this case // the key code to be transmitted should have the MSB set if a shift key is down. if (ScanCode != shiftleft && ScanCode != shiftright) { if (KeyPressed[ScanCode] == 1) { Serial.print("Scan>"); //so you can see the byte Serial.println(ScanCode); //so you can see the byte //translate key to map index map_index = 10 * (ScanCode >> 4) + (ScanCode & 15); Serial.print("MI:"); //so you can see the byte Serial.println(map_index); //so you can see the byte if (ShiftStateWhenKeyPressed[ScanCode]) { ch = map_shift[map_index]; } else { ch = map_plain[map_index]; } //if sendch(ch,0,0); } // key just released KeyPressed[ScanCode] = 0; // Note that the key was down for all keys including shift ShiftStateWhenKeyPressed[ScanCode] = (ScanCode == shiftleft || ScanCode == shiftright); } // if key other than shift KeyPressed[ScanCode] = 0; } //not detected // Finished with this key. Move on to the next. if (column == 9) { column = 0; if (row == 7) { row = 0; } else { row++; } } else { column++; } } //main loop