Actionwriter keyboard conversion for PC

or, how to get a Model Mish for only $10ish

PLEASE NOTE: This article is a work-in-progress

I'm a fan of mechanical keyboards. Now, I'm nowhere near as rabid of a fan as others who will hundreds of dollars on them (I'm actually kind of a cheapskate) but I have always wanted to own a Model M. I also have something of a soft spot for old typewriters, and I buy them at garage sales when I can. Of course, the logical intersection of these two interests is typewriter keyboards. A lot of old electromechanical typewriters have cool keyboards - both for the switches and the layout. I found an IBM Actionwriter 1 at a garage sale and these are the notes of how I've turned it's keyboard into a PC-compatible USB keyboard.

How a buckling spring keyswitch looks and sounds: Buckling spring keyswitch diagrammatic animation

The venerable Actionwriter is a electromechanical typewriter, meaning it has a keyboard that is connected to a microcontroller that in turn drives the mechanical equipment doing the type-writing. Most importantly, being a genuine 1985 West Germany-made IBM product it had a buckling spring keyboard. If you aren't up to date on keyswitch types, the buckling-spring is something of a holy grail (IMO, at least.) They have a great kinetic feel and are gloriously clicky. Unfortunately, Model Ms are kinda expensive because no matter how you cut it you have to have one of these complicated little assemblies for each key.

To make the most of this article, you should know a little about programming and a little more about electronics. The majority of the code involved is just shuffling around data, so you should be able to get by as long as you can read C and Python. As far as the hardware, if you've used an Arduino before your probably good - I went into this having never meaningfully used ICs.

How does this thing connect, anyway?

The first step was to pop the top off the typewriter and take a closer look at the keyboard. The typewriter hides it's guts between two halves of a plastic shell that pop apart neatly to reveal it's inner workings. The keyboard itself then can be unplugged from the main PCB by removing the three connectors. The keyboard is built on top of a single curved metal piece that also has a cutout to hold the status LEDs. The ribbon cable for driving the LEDs is one of the connectors back to the PCB. The other two are "Flexible Flat Cables" (FFCs) - you may have had the displeasure of working with these if you've ever torn apart a laptop or similarly compact device. These FFCs are not actually so bad - the cables and their contacts are pretty big.

The microprocessor on the board in the Actionwriter is some sort of 80xx (CHIP?) but the keyboard interface is obviously not PS/2, AT, or even that weird one that looks like SATA. A keyboard has to have a switch somewhere inside it for every key, but it would be absurd to wire every switch to it's own line - I/O pins are expensive! Manufacturers wire the keys together in a "keyboard matrix" to enable reading all of the inputs with a reasonable number of I/O lines. The keyboard, intended to be used only in the typewriter, exposed only the raw keyboard matrix lines.

Reading a keyboard matrix
LOADING
  

The principle of a keyboard matrix is that we can select which out of a number of switches wired in parallel we want to read by driving the input of only one of them. This can then be extended to select a number of switches, each with their own input line. In this manner, if we have n I/O lines we can read ⌊(n/2)^2⌋ switches. The downside is that to read the state of all the switches we must go through a cycle of scanning each of the ⌊n/2⌋ banks of switches.

The animation to the right depicts a matrix of 16 keys, where O represents open and X represents closed. As the selected column scans across each of the four (indicated by capitalization) we read each of the four rows, filling out columns in out table that eventually holds a complete image of the state of the keyboard. It's a lot like addressing a 2-dimensional array. Some helpful articles about this system with better illustrations are here and here. Code for this process is pretty simple and looks like:

bool state[NR_ROWS][NR_COLS];
...
void update_state(){
	memset(state, 0, sizeof state);
	for(int col=0; col<NR_COLS; col++){
		setColumnHigh(col);
		for(int row=0; col<NR_ROWS; row++){
			state[row][col]=readRow(row);
		}
	}
}

I bought two CONN FFC VERT 14POS 2.54MM PCBs from DigiKey, who since seem to have discontinued them. These are ZIF connectors for the FFCs on the keyboard to plug into. 2.54mm (0.1in) is the standard spacing for breadboards, and apparently also (or at least close enough to) the spacing of these FFC cables. The two connectors for the keyboard have 13 and 8 connected leads (the second has 15 connectors total, where the last 7 are connected to each other but not to the keyboard for some reason.) I stupidly bought 14-lead connectors, but I made it work by clipping off some of the extra plastic on the end of the 15-lead one that is all wired together.

Now that I had them in front of me, I could hook them up to an Arduino Leonardo (selected for it's USB emulation capability) and make sure the connectors from the keyboard really did connect to the rows and columns of a matrix of switches. Yes, they do. However the 21 meaningful I/O lines required to read the keyboard's 64 keys were beyond the Arduino Leonardo's 13 I/O lines And that's not even thinking of other things I might want I/O for on the keyboard controller.

Multiplexing our Matrix
Two 8:1 muxes as a 16:1:
  
 dec

There is a pretty elegant solution to this problem. I bought a couple 8:1 de/multiplexers on DigiKey (part nr. CD4051BE.) These guys allow you to control which of their eight output pins are electrically connected to their one input pin by driving it's address lines. A de/multiplexer with n address lines allows selection between 2^n different lines, so for an mux with eight lines there are log2(8) = 3 address lines. Because we actually only ever want to drive the selected column high, and there is no reason we would want to not select any column, we don't actually need to spend an I/O pin on the input to the mux, we can just connect it to +3v3.

I've called this chip a de/mux because it can be used to either control which of eight outputs is attached to an input as well as be used to control which of eight inputs is attached to an output. It's best thought of as a rotary switch connecting the one pin to one of the eight - this connection is bidirectional. Because of this, we can use it to select between which of the eight rows of the matrix we want to read - so we can use log2(8) = 3 lines for address select and one for input to read all eight rows.

It's not quite that simple for selecting our column, because there are not eight columns, but 13. 13 isn't even a power of 2! To select which of the 13 columns to light up, we will need to use ⌈log2(13)⌉ = 4 address bits. We also need some way to join two 8:1 muxes into a 24 = 16:1 mux. We can do this with a discrete transistor acting as a NOT gate. These multiplexers have a disable pin, which means that the one pin is disconnected from all of the eight. If we connect the high address bit to the disable pin of one mux, and the logical inverse of the high address bit to the disable of the other mux we can use it to switch between them. Then, we connect the low three address bits to the address inputs of both muxes, joining them together into a 16:1 multiplexer. Selecting which column to light up is as simple as writing the bits of it's index out to the address channels:

void setColumnHigh(int address){
  if(!((address>=0)&&(address<=15))) error();

  digitalWrite(COL_MUX_ADDRESS_PINS[0],  bitRead(address, 0));
  digitalWrite(COL_MUX_ADDRESS_PINS[1],  bitRead(address, 1));
  digitalWrite(COL_MUX_ADDRESS_PINS[2],  bitRead(address, 2));
  digitalWrite(COL_MUX_ADDRESS_PINS[3],  bitRead(address, 3));
}

Reading one of the eight rows through the mux is the same, but we only write log2(8) = 3 address bits, and do a read from the :1 side after.

Putting the map back together

There are a couple things about the real system that are different from what I've been describing. If you've been reading the tooltip text you'll have seen some of them. The two important things are:

  1. The keyboard only has 64 keys, but there are 104 places in the 13x8 keyboard matrix. Why? I'm not sure, but I have a hunch. The IBM Wheelwriter 5 shares the same keyboard layout as the Actionwriter 1, with the addition of five more keys on the left hand side (you can see that on page 12 of this pdf.) I suspect it shares the same membrane and metal backplane as the Actionwriter, since if you take apart the keyboard from this typewriter there's another row of five contact pads without hammers/stems immediately to the left of the leftmost column of keys. On this subject, there's also a pad and stem immediately to the right of the brackets keys (under the top third of the return key, where backslash would normally be.) If I find aesthetically matching keys I may mod this keyboard to have a backslash and normal-sized enter. Regardless, the question still stands as to why not use a 9x8 matrix.

    The important part is that there are "holes" in the matrix where no physical key will cause it to read connected. Unfortunately each of the 13 columns has at least one key in it, so we have to at least scan across them all. The real keyboard controller program avoids reading the state of the holes, though.
  2. If you've worked with this kinda DIY electronics before you'll have hit issues with bounce and noise left and right. One thing I did in this project that was pretty effective at helping the bounce/noise issues was to make it so "driving" a column meant grounding it. Then I enabled the built-in pull-up resistor on the pin I read from on the Arduino so that if it's not connected it will read high. It's then inverted in software. This really helps - I don't know enough about electronics to know why though :P

At this point there wasn't anything left to do besides put it together in hardware! The minimum necessary to be able to read the whole keyboard matrix is 3 8:1 muxes and the transistor to switch between them. You could probably go without the mux on the rows and the switching transistor if you wanted. The controller as it exists on my desk is only a little more complex than what I've described, because it has some hardware (shift registers) to run status LEDs.

Once it's wired up the next step was to figure out the mapping of (row, col) matrix positions to keys. For this I wrote a small script for the Arduino that just dumped the entire state of the keyboard matrix over serial. This isn't how real keyboards work, and it sends a bunch of extra info (the holes in the matrix.)

const int COL_ADDRESS_PINS[4] = {0, 1, 4, 5};
const int ROW_ADDRESS_PINS[3] = {6, 7, 8};
const int ROW_READ_PIN        = A0;

void setup(){
	//Configure the pins. Switch circuit is closed when it's low, so set the
	// input pin to be high by default
	pinMode(ROW_READ_PIN, INPUT_PULLUP);
	for(int i=0; i<4; i++) pinMode(COL_ADDRESS_PINS[i], OUTPUT);
	for(int i=0; i<3; i++) pinMode(ROW_ADDRESS_PINS[i], OUTPUT);

	Serial.begin(115200);
	while (!Serial); //Initialize serial port and wait for connection
}

void loop(){
	Serial.print("[");
	for(int col=0; col<13; col++){
		setColumnHigh(col);
		for(int row=0; row<8; row++){
			Serial.print(readRow(row));
		}
	}
	Serial.println("]");
}

void setColumnHigh(int address){
	for(int i=0; i<4; i++)
		digitalWrite(COL_ADDRESS_PINS[i], bitRead(address, i));
}

bool readRow(int address){
	for(int i=0; i<3; i++)
		digitalWrite(ROW_ADDRESS_PINS[i], bitRead(address, i));

	return !digitalRead(ROW_READ_PIN);
}

There's a Python script in the repo for this project that displays the keyboard matrix as you type in real time. I used this to figure out the locations of keys in the matrix. This process was as simple as pulling up this tool and pressing keys, while writing the resulting (column, row) coordinates down in a text file. This then gets parsed into a python data structure that is a dictionary associating (column, row) indices in the matrix with (arbitrary) pretty key names. The darker gray squares are the holes discussed earlier. (12, 2) is where backslash would be if there was a key to actuate it. It looks like this:


Arduino Contraption → Keyboard

One quick optimization we're going to do is to make it so that we only care about the parts of the keyboard matrix that map to actual keys. The keyboard map above clearly indicates that holes make up something like 40% of the matrix. We will compute a bitmask representing which places in the matrix we care about, and we can have the Arduino only bother to read those spots.

from mapping import mapping #Map of (col, row) to key name

columns=[[] for _ in range(13)]
for pos in mapping.keys():
	columns[pos[0]].append(pos[1])

masks=[]
for col in columns:
	masks.append([i in col for i in range(8)])

bitmasks=[]
for mask in masks:
	bits=reversed(map(str, map(int, mask)))
	#    ^switching endian so that the 0th bit is the 0th row
	bitmasks.append("0b"+"".join(bits))

print(bitmasks)
//Into C syntax...
const unsigned char
MASK[13] = {
	0b00000001,
	0b10001011,
	0b01101111,
	0b00000001,
	0b01111010,
	0b01011010,
	0b01011010,
	0b11111111,
	0b11111111,
	0b01111110,
	0b01011010,
	0b11111101,
	0b01111011
};

You can see that the resultant masks represent the same information as the keymap image above, but it's now column-major instead of row-major (i.e. it's been rotated 90° right.) This is because we will be scanning column-by-column. The other thing we are going to need to do is keep track of the state of the keyboard. USB Keyboards send events to inform the computer in the changes of the states of keys, so we need to keep track of the previous state so that we can send these events on the rising and falling edges.

Next, we'll generate the mapping from these linear indexes to the keycodes we want to send using the Aruduino Leonardo's Keyboard emulation. For letters and numbers this will be as straightforward as just spitting out the character. For others (e.g. ctrl) we will need to use the #defines from the Arduino keyboard library.

from mapping import mapping

columns=[[] for _ in range(13)]
for pos in mapping.keys():
	columns[pos[0]].append(pos[1])

keymap=[]
for col, entries in enumerate(columns):
	for row in sorted(entries):
		keymap.append(mapping[(col, row)])

print(keymap)

for_arduino=[]
for item in keymap:
	if len(item)==1 and item in "abcdefghijklmnopqrstuvwxyz":
		for_arduino.append("'%s'"%item)
	else:
		for_arduino.append({
			"code": 	"KEY_LEFT_GUI",
			"mar_rel": 	"KEY_ESC",
			"r_mar": 	"KEY_LEFT_ALT",
			# ...a bunch more omitted here...
			"topleft":	"'~'",
			"semicolon":	"';'"
		}[item])

print(", ".join(for_arduino))

Then we take the output of this and plug it into an updated version of the previous controller code. We've also updated the controller to scan all 64 keys into a buffer, and to keep two copies of this buffer. We then send keyboard events on the rising and falling edges of all of those keys. The delay(10) is a lazy form of debouncing. A better approach would be to have an array int cycles_down[64] and determine some number of cycles through loop() to require a key to be down before counting it. This, however, is simple and the latency isn't a big deal. Putting it together, these are the interestingly changed parts of a minimal keyboard controller:

#include <Keyboard.h>

//...

const unsigned char MASK[13] = {
	0b00000001,
	0b10001011,
	0b01101111,
	0b00000001,
	0b01111010,
	0b01011010,
	0b01011010,
	0b11111111,
	0b11111111,
	0b01111110,
	0b01011010,
	0b11111101,
	0b01111011
};

const unsigned char MAP[64] = {KEY_LEFT_GUI, ' ', KEY_END, 0,
	0, KEY_DELETE, KEY_LEFT_CTRL, KEY_LEFT_ALT, '\t', KEY_ESC,
	0, KEY_LEFT_SHIFT, 'z', 'q', '1', '`', 'a', 'x', 'w', '2',
	's', 'c', 'e', '3', 'd', 'b', 'v', 't', 'r', '4', '5', 'f', 'g', 'n', 'm',
	'y', 'u', '7', '6', 'j', 'h', ',', ']', 'i', '8', '+', 'k', '.', 'o', '9',
	'l', '/', '[', 'p', '0', '-', ';', '\'', KEY_INSERT, KEY_RETURN,
	KEY_PAGE_DOWN, KEY_PAGE_UP, KEY_BACKSPACE, KEY_HOME};

bool state[64];
bool old_state[64];

void setup(){
	//...

	Keyboard.begin();

	memset(old_state, 0, sizeof state);
}

void loop(){
	memset(state, 0, sizeof state);
	//Since we only write into the buffer sometimes, keep a count
	int state_pos = 0;

	for(int col=0; col<13; col++){
		setColumnHigh(col);
		//Have to do this because we have to scan all columns
		for(int row=0; row<8; row++){
			if(bitRead(MASK[col], row)){
				state[state_pos] = readRow(row);
				state_pos++;
			}
		}
	}
	
	for(int i=0; i<64; i++){
		//For each key, send a press on the rising edge
		// and a release on the falling edge
		if(state[i] && !old_state[i]){
			Keyboard.press(MAP[i]);
		}else if(!state[i] && old_state[i]){
			Keyboard.release(MAP[i]);
		}
	}

	//Switch buffers
	memcpy(old_state, state, sizeof state);
	delay(10);
}

This is a really minimal version. You'll see a couple 0s in there for keys that I haven't yet bound to anything: physically these are the LMar, TSet and TClr keys. It also simply has no way to type a bunch of stuff: backslash, F-keys, arrow keys or caps lock. Of course, this mapping is essentially arbitrary. If you buy one of these guys you can configure the keyboard to have whatever layout you'd like. The biggest issue is that the keyboard for the Actionwriter 1 simply has less keys than a normal computer keyboard.
So: how to deal with this? Shifting!

Keyboard customization for fun and profit

You may have wondered at this point: Does the shift key work on this keyboard? The answer is yes! When you press Shift, it is reported to the computer as any other key, and the actual shifting is done inside your OS. In fact, the whole idea that we are assigning characters to keys is a little disingenuous - we're actually assigning scancodes to keys. The Arduino Keyboard library just makes it so that we can use characters and their constants to refer to the scancodes by what they map top on a US-Layout keyboard. That means that if you flash your Arduino with the code above and plug it into a system with a Workman layout the keycap with D printed on it will type an H.

The kind of shifting I have in mind to solve the too-few-keys problem is different. It's more like the "shifting" done by the Fn key on a laptop or compact keyboard. When you press the Fn key, it's not reported to the OS. It instead causes the microcontroller in the keyboard to emit different scancodes to the computer when you press the Fn key in concert with different physical keys. In this way, (some of) the physical keys on the keyboard end up with multiple scancodes: one normal, and one when "shifted" with the Fn key. This is what we are going to do on the Actionwriter keyboard, to allow you to use a hardware meta key to input keypresses for keys that the keyboard does not physically have.

The easy approach is to simply factor out the logic of going linear-matrix-code → key into it's own function, then make decisions on how to overload that based on what keys are held down at the moment. We will do this, and then later look at a more complex but more elegant approach. The first thing we want to do on this front is to make it easier to identify the keys in the software running on the Arduino. This will allow us to use meaningful names instead of arbitrary numbers in our shifting logic.

(Extending the previous Python snippet...)
with open("defines.c", 'w') as fd:
	for idx, item in enumerate(keymap):
		fd.write("\n#define LK_"+item+" "+str(idx))
#define LK_code 0
#define LK_space 1
#define LK_repeat 2

There are, of course, a lot more lines in the output than what I have on the right. I've just cut them off because they aren't very interesting. This represents the mapping from meaningful names to our arbitrary linear IDs. This allows us to write more complex code on the Arduino without magic numbers.


I'm Louis. Contact me at louis@goessling.com or find me on GitHub at 602p.
I go to the U of MN. I'm the Sysadmin for the ACM chapter there.
There are a handful of other things on this website.

Creative Commons License
This article, "Actionwriter Conversion" (including those original images and assets not named below) by Louis Goessling is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

CC-BY-* credits

  • New Window/Popup icon () by Denis Klyuchnikov from the Noun Project
  • Buckling Spring diagram by Shaddim - Own work, CC BY-SA 3.0, Link
  • The buckling spring audio clip is public domain from Wikipedia