When we put together the ZRT-80 it was complete luck that there was an unassembled kit for an ascii keyboard to just use for that project. Plenty of people aren’t so lucky and have to resort to extremely expensive vintage offerings or converters that use pretty outdated keyboards as the base anyhow. I can’t exactly throw stones because I’m still using an older keyboard every day and the matrix I chose for this project is not really great quality, but at least it was free. Well, at least it was free to me. The archer 227-1020 keyboard is not exactly cheap these days if you want to do an exact clone of this project. EDIT: this is a Coleco Adam keyboard. In fact, to interface with my matrix you need a nearly unobtainable 2.5mm spacing flat flex socket that’s exactly 21 positions wide. Almost no one makes those connectors in that spacing any more and only one that I could find that wide.
Even if you don’t want to spend an unreasonable amount of money on a hard to use and poor feeling keyboard, this project can help with other options. We live in the golden age of custom keyboards! standardized switch shapes and footprints, custom printed and molded keycaps, diy frames and 3d printed cases, you could do everything that people do to make custom USB keyboards and replace the controller with mine here and you have an extremely high quality parallel keyboard for any old ’70s or ’80s system that needs it. If you’re doing your own matrix and can lay it out however you want then you might look at the 2376 encoder chip which (with minimal extra parts) can serve as a one-chip solution parallel keyboard.
Once I had obtained the connector for the key matrix I had to decide on a strategy for talking to the keyboard and the computer/terminal. I needed 21 pins for the matrix, 8 data bits, and a strobe line. I read somewhere that slower computers have a busy line so they can keep the keyboard from sending more data until the computer is ready so we can also add that. I’m up to 31 pins and I don’t even have any LED indicators or aux switch inputs/outputs. At that big of a pin count my microcontroller options are annoyingly expensive so we’re back to the old atmega328 and associated chips to reduce the pin count. Above you can see 2x 74hc138 chips wired together as a 4-to-16 matrix scanner, a 74hc595 as a SPI to parallel output chip for driving the bus to the computer, a 74hc597 as a SPI to parallel input for reading the matrix, and a 74hc86 xor gate for selectable inversions of the strobe and busy lines. I’m pretty proud of this setup but it needed a little rework after first contact with the matrix.
Here is my brilliant attempt to scan the key matrix. These ‘138 chips output a high logic level on all the outputs except the one that’s selected. These days chips tend to go into high-z mode on the lines that aren’t being used but in some applications driving outputs like this would reduce the need for pull up resistors and cut down on the part count of the design. I used one of the positive enable lines and one of the negative enable lines to add a fourth address line to these two chips, making it act like one big one. This meant that I just needed one extra pin on the arduino and I could double my scanning output. This worked, but if you look up at the matrix layout above we have a problem. When you press and hold one key while pressing another it is possible to interfere with the scanning and give the voltage somewhere else to escape. On this matrix the modifier keys, control and shift, are in the matrix, but not on any shared rows. This means they can be scanned by the matrix and if you do it right they will not interfere with any other keys. I did it wrong. Holding shift and A for example connects two of the outputs of the ‘138s together, inevitably connecting a high output to a low output whenever one of those rows is scanned. I then had to reverse course.
Here is my revised (cut down) strobe circuit, now only scanning the 8 columns. This also shows my use of XOR gates for selectable inversion of the strobe and busy lines. With this configuration you can run the keyboard with the busy line unconnected and the pull up will make it think that the connected computer is always ready for more data. If the computer needs inverse polarity of either or both of those lines just short those jumpers and you can invert the signals in hardware. If you want a really general purpose keyboard I guess you can break those out to toggle switches, but usually something like this would be paired with a specific computer or terminal and they can be set once (also possible to do in software, but I find it annoying to have to recompile some software every time I want to change a little setting like that).
Here is the new 597 chained input section. This can read up to 16 lines of the matrix all latched in at once and using hardware SPI too! The 595 adds 8 output pins and only costs one more pin on the arduino as they are the same bus. The chainable output on the 595 is left floating because we don’t need to drive another chip (although we could) and the 597s are tied together with theirs. This would have been a perfect time to use SIP resistor packages but I didn’t have any of the right size convenient. I can use the unused input lines to detect if there’s a problem with the hardware or the reading routine for the chips. The top 3 lines are always wired to 5v through their pull up resistors, the unused chainable input is wired to ground. That means if I read anything other than high logic signals on the lines that are supposedly unconnected I have an issue, and if I read three or more bytes and the additional bytes are anything other than low logic signals I also have an issue.
The hardware was the easy part, the software is where things are more interesting. Driving the ‘138 scanner is trivial, just put the address on the bus. Converting from a byte to individual lines though is nice and compact: We iterate over the entire array of output pins, writing the result of bit shifting and checking if the resulting number is even or odd, basically if the least significant bit is 0 or 1. The bit shifting just puts that bit in the position to drive the output of that modulus check.
//set address line
address += 1;
if (address > 7)
address = 0;
for (int i = 0; i < sizeof(addr_pin) / sizeof(addr_pin[0]); ++i)
digitalWrite(addr_pin[i], (address >> i) % 2);
delay(1);
Then we have to read from the ‘597s. We take that data into a 2-byte wide variable and similarly check if the bit at a given position is a one or a zero. In this case a zero means the key is being detected as down. we then update a current working copy of all the key statuses (if they are pressed or released)
SPI.beginTransaction(set597);
digitalWrite (ss, LOW);
digitalWrite (ss, HIGH); //Latch inputs
//read entire row of keys
temp = SPI.transfer(0);
temp += (SPI.transfer(0) << 8);
SPI.endTransaction();
for (int i = 0; i < 16; i++) //iterate through each bit of the 16 lines we just read
{
if (!((temp >> i) % 2))
{
key_status[i][address] = 1;
}
else
{
key_status[i][address] = 0;
}
}
The next part is a bit hard to follow. We iterate through the whole matrix (8×16) checking the key statuses versus what they were last time we scanned them. If they move from a 0 to a 1 the key must have been just pressed, if it went from a 1 to a 0 it must have been released. On key down, if the key is shift or control and nothing else is complicating things we move into either control or shift mode. In these modes the keyboard keys are fully remapped to send different codes, all definable in tables. On key release if you are releasing the shift or control keys we cancel that mode unless some lock has been set. This keyboard has a lock key so I decided to make shift lock and control lock each work as if you are holding down those keys all the time. The lock status is indicated by the state of two LEDs that can be wired to the top of a future enclosure. If a key goes down and you are in either shift or control mode then we check to see if it’s the lock key and either set or un-set the lock mode. The last thing we do after checking each key is to update the other table of key statuses so we can detect the next time it goes up or down properly.
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 16; j++)
{
if (prev_key_status[j][i] && !key_status[j][i]) //key went up
{
if (!controled && !shifted)
{
//do nothing
}
if (i == 0 && j == 13 && !shift_locked)//hardcoded shift
shifted = 0; //only stop shifting if the lock isn't set
if (i == 0 && j == 12 && !control_locked)//hardcoded control
controled = 0; //only stop controling if the lock isn't set
}
if (!prev_key_status[j][i] && key_status[j][i]) //key went down
{
if (controled)
{
if (i == 0 && j == 14 && !control_locked)//hardcoded lock
{
control_locked = 1; //set control lock
digitalWrite (ctrl_lock, control_locked);
Serial.println("set control lock");
}
else if (i == 0 && j == 14 && control_locked)//hardcoded lock
{
control_locked = 0; //un-set control lock
digitalWrite (ctrl_lock, control_locked);
Serial.println("un-set control lock");
}
else
{
HandleKey(j, i, 0, 1);//controlled key
}
}
else if (shifted)
{
if (i == 0 && j == 14 && !shift_locked)//hardcoded lock
{
shift_locked = 1; //set shift lock
digitalWrite (shift_lock, shift_locked);
Serial.println("set shift lock");
}
else if (i == 0 && j == 14 && shift_locked)//hardcoded lock
{
shift_locked = 0; //un-set shift lock
digitalWrite (shift_lock, shift_locked);
Serial.println("un-set shift lock");
}
else
{
HandleKey(j, i, 1, 0);//shifted key
}
}
else if (!controled && !shifted)
{
// modifier keys handled state triggered now, not edge triggered
HandleKey(j, i, 0, 0);//normal key
}
}
prev_key_status[j][i] = key_status[j][i];
}
}
I still had some edge cases based on how the matrix was scanned so I had to add some checks outside of the key edge detection logic, I added some state dependent logic. I may be able to go entirely to state based logic but for now this works. Depending on what order I scan that far left column it is possible to detect two keys going down in the same scan window and act on them in whichever order I want, but that’s probably too far in the weeds for a matrix that scans so fast.
//this handles edge cases where you unlock while not holding down the modifier key
if (!key_status[13][0] && !shift_locked)
shifted = 0;
if (!key_status[12][0] && !control_locked)
controled = 0;
//this handles edge cases where you unlock while holding down the other modifier key
if (key_status[12][0] && !key_status[13][0] && !shift_locked) //holding control and not holding shift and not shift locked
controled = 1;
if (key_status[13][0] && !key_status[12][0] && !control_locked) //holding shift and not holding control and not control locked
shifted = 1;
The HandleKey function basically replaced my debug statements, taking all the data I used to print out and handling it sanely.
void HandleKey(int j, int i, boolean shift, boolean control)
{
if (!shift && !control)
{
if (key_scancode[j][i] > 0x7f)
HandleMacro(key_scancode[j][i]);
else
SendKey(key_scancode[j][i]);
}
if (shift)
{
if (shifted_scancode[j][i] > 0x7f)
HandleMacro(shifted_scancode[j][i]);
else
SendKey(shifted_scancode[j][i]);
}
if (control)
{
if (controled_scancode[j][i] > 0x7f)
HandleMacro(controled_scancode[j][i]);
else
SendKey(controled_scancode[j][i]);
}
}
The next function is something I’m pretty happy with. Macros can be any character from 0x80 to 0xFF, I started counting backward from 0xFF because it’s easy to see all the undefined 0xFF key behavior in the scancode tables. This means that in my current implementation you cannot define extended ascii characters except through single character macro sequences which is a bit cumbersome to read but I don’t expect to come up often, if at all. I don’t actually know how various terminals and computers will respond to getting 8-bit extended ascii sequences over a parallel keyboard port, the 2376 controller chip won’t generate them so maybe they’re just passed on to the operating system or program? maybe they’re filtered out? maybe they just ignore the high bit entirely.
These macros can be useful for talking to a specific terminal and automating escape or control sequences. They can also be useful for talking through the terminal transparently and talking to the computer operating system or program to execute a specific function that could take multiple keys.
void HandleMacro(int macro)
{
switch (macro) {
case 0xff:
Serial.println("do nothing");
break;
case 0xfe:
Serial.println("up"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
//SendKey(0x31); //1 might be unneeded, defaults to 1
SendKey(0x41); //A
break;
case 0xfd:
Serial.println("down"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
//SendKey(0x31); //1 might be unneeded, defaults to 1
SendKey(0x42); //B
break;
case 0xfc:
Serial.println("left"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
//SendKey(0x31); //1 might be unneeded, defaults to 1
SendKey(0x44); //D
break;
case 0xfb:
Serial.println("right"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
//SendKey(0x31); //1 might be unneeded, defaults to 1
SendKey(0x43); //C
break;
case 0xfa:
Serial.println("wtf? no. stop."); //you are mucking with control and shift at the same time, stop it!
break;
case 0xf9:
Serial.println("Erase in Display"); //from wikipedia ANSI_escape_code
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x32); //2
SendKey(0x4a); //J
break;
case 0xf8:
Serial.println("home"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x48); //H
break;
case 0xf7:
Serial.println("F1"); //from wikipedia ANSI_escape_code
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x31); //1
SendKey(0x50); //P
break;
case 0xf6:
Serial.println("F2"); //from wikipedia ANSI_escape_code
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x31); //1
SendKey(0x51); //Q
break;
case 0xf5:
Serial.println("F3"); //from wikipedia ANSI_escape_code
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x31); //1
SendKey(0x52); //R
break;
case 0xf4:
Serial.println("F4"); //from wikipedia ANSI_escape_code
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x31); //1
SendKey(0x53); //S
break;
case 0xf3:
Serial.println("normal charset"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x67); //g
break;
case 0xf2:
Serial.println("alt charset"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x66); //f
break;
case 0xf1:
Serial.println("clear display"); //from zrt-80 manual
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x45); //E
break;
case 0xf0:
Serial.println("clear"); //spare
SendKey(0x1b); //ESC
SendKey(0x5b); //[
SendKey(0x32); //2
SendKey(0x4a); //J
break;
default:
Serial.println("undefined macro");
break;
}
}
The last part is how I deviate a little from the 2376 I’m modeling my timing on. That controller holds a state for as long as you are holding down that key. With my multi-key sequence macros I can’t do that. I took the information on the world’s fastest typist (216WPM), converted that to characters per minute (1080) and then to the maximum amount of time the whole transaction needs to take place (55.5ms). That helped me set the timing of the keyboard pulses so as to mimic someone typing faster than the world’s fastest typist hopefully to allow for the whole macro to be sent out before someone tries to hit the next key (although for very long macros that time would have to be further reduced to get that effect).
void SendKey(byte code)
{
//set bits
SPI.beginTransaction(set595);
digitalWrite (ss, LOW);
//set output bits
SPI.transfer(code);
digitalWrite (ss, HIGH);
SPI.endTransaction();
Serial.println(code, HEX);
//settle 4.4ms + 88cycles @ 50khz = 6.16ms
delayMicroseconds(6160);
//set strobe
digitalWrite (strobe, HIGH);
//settle 0.5us + 30ms (way exceed human typing speed)
delay(30);
//look for busy signal
while (!digitalRead(busy)) { //blocks on busy signal being low
Serial.println("blocking for busy line");
}
//un-set strobe
digitalWrite (strobe, LOW);
//settle for more than 0.5us
delayMicroseconds(1);
}
The keys I chose for the macro sequences came from the manual for the ZRT-80 (which this will probably hook up to), the manual for the ADM-3a (a pretty standard terminal), and the wikipedia entry on escape sequences. I will probably want to tune the macros to the application, but for now these will do. To edit the codes that each key sends just edit the three tables near the top of the code, 0x00 indicates no key is present in the matrix (or to send the null byte), 0xff indicates that a function has not been assigned to that key in that mode, anything 0x7f or less just gets sent right out, anything above that gets handled by the macro function to send one or more keys in sequence. I wired the output exactly like a parallel port that printers hook up to so if I were to get a dot matrix printer I may be able to make this act like a teletype in loopback mode. Now I just have to model up and print an enclosure. Either that or I can just buy a metal keyboard case and cut a big hole for the keys to stick out.