The 1986 vintage keyboard
The original TRS-80 Model 100 was released in 1983, but three years later the Tandy 102 came out which was a thinner version of the same PC, and used surface mount technology which resulted in a cost savings for the consumer.
But what’s even better than the cheaper price is the fact that the Tandy 102 keyboard actually feels better, as the key-switches are an improved model from Alps. As a touch typist, typing on the 102 is quite satisfying and it is my favorite keyboard of the entire “Model T” lineup. (Although I believe the same key switches were used on the flip-up Tandy 200 computer as well.)
I had one Tandy 102 with a dead motherboard but had a keyboard it absolutely awesome cosmetic shape. Of all of the keyboards I could find, its key caps had the least amount of yellowing. But, a few of its keys didn’t actually work. A bit of troubleshooting with a multimeter revealed the cause to be a trace on the PCB that had been eaten by corrsion. I simply soldered in my own wire to bypass the corroded trace, and finally the keyboard was all working.
The cable that attaches the 102 to its motherboard is a flat ribbon type with an edge connector at both ends. This presented its first challenge: I didn’t want to have to solder a bunch of wires onto the connecting pads one the end of the ribbon cable. Thankfully a friend of mine looked up and found a part on Mouser.com that had the right pitch and height for this type of ribbon. This meant I could use the 102’s keyboard for my speciality purpose but in a non-destructive fashion.
Next was the business of soldering my own patch-wires to that edge connector:
Once I had all 18 contacts wired up, it was a simple matter to get it all connected to my breakout board that connected to my 40-pin ribbon for the PINE A64’s GPIO connector. By the way, the GPIO connector conveniently has the exact same pinout as the Raspberry Pi 2. Must mean there are plugin boards available that work on both the A64 and the Pi..
I had the PINE set up with a regular USB keyboard attached while I was working through getting the 102 keyboard working with the GPIO signals. As I was working through a couple of issues I came to the realization that the CAPS-LOCK and NUM-LOCK keys on the Tandy 102 keyboard were going to present a problem.
Both the Model 100 and Tandy 102 keyboards have mechanical locking keyswitches for those two keys. This means when you hit CAPS-LOCK or NUM-LOCK, they “engage” and stay pressed down in a lower position. You hit them again to release them. Well, for modern OS’s, that isn’t how those features work. On any modern computer, you simply tap CAPS-LOCK or NUM-LOCK to engage and tap them again to disengage that mode, and it is typically indicated by an LED on the keyboard itself.
My Tandy 102 keyboard presented a problem for these features and it became clear that the mechanical locking keyswitches themselves would need to be swapped out and replaced with “regular” momentary-contact keyswitches from a donor keyboard which I had in my stash of “just for parts” machines. That way, they would no longer lock down and instead just send a signal and release normally, and would function just like any modern PC keyboard you use.
But there’s a catch: On the Model 100/Tandy 102 type of keyboard, those mechanical locking keyswitches are actually wired on the keyboard PCB slightly differently; the terminals on the locking keyswitches close the left pin and right pins together, but regular keyswitches close the top and bottom pins. I had to cut some traces on the keyboard’s PCB, and solder in my own bodge wires to get the wiring right on those two spots on the keyboard PCB that are set up for the regular mechanical locking switches.
After desoldering the original CAPS-LOCK and NUM-LOCK keyswitches and soldering in new momentary-contact keyswitches from another donor keyboard, this thing was finally ready to be used as the permanent keyboard in my new “PINE 100” computer.
The keyboard driver
Belsamber’s python script was a good starting point but I really wanted to have additional key mappings to support the “num-lock” feature as well as the characters that just don’t have individual keys on this keyboard.
And so I began the extra coding for allowing SHIFT and CODE keys to modify other keys appropriately for extra functionality. There are several keys that are just not present on the keyboard physically that us “modern” computer users expect, but not only that, one peculiar thing about this keyboard is the right brace key is generated by hitting shift left brace. There’s also no DEL key, no backslash, vertical bar, or curly braces.
Here’s the stuff I had to implement specially:
Num-lock functionality, which modifies the U,I,O,J,K,L, and M keys
SHIFT BS generates DEL
SHIFT [ generates ]
CODE / generates \
CODE 1 generates |
CODE 9 generates {
CODE 0 generates }
CODE F5 generates F9
CODE F6 generates F10
CODE F7 generates F11
CODE F8 generates F12
The OS doesn’t even really care about the num-lock signal itself other than to allow an on-screen indicator to show the status. The actual feature of generating numbers based on the regular keys (U,I,O,J,K,L, and M) is done entirely within the driver based on a num-lock state that it maintains in a variable.
Here’s the Python keyboard driver in its entirety:
#!/usr/bin/python3
import RPi.GPIO as GPIO
from time import sleep
from evdev import UInput, ecodes as e
import logging
# ================= Special keyboard handling [GW] =================
# Support for Num-Lock with the numlock_keymap. This requires a modified Tandy keyboard that has the Num-Lock
# mechanical locking keyswitch replaced with a momentary-contact keyswitch, like that of most keys on the keyboard.
# This means when a user presses Num-lock, it will simply switch the state and not remain locked down in the ON position.
# numlock_keymap[] contains the modified keyboard layout including the numeric keys for m,j,k,l,u,i and o.
#
# A lot of CODE keys require two events to be generated (shift + some other key), so special logic had to be created
# for those that couldn't be represented in the code_keymap.
#
# Here's the list of SHIFT & CODE keys requiring special handling:
#
# SHIFT BS = DEL : unpress KEY_LEFTSHIFT, press KEY_DELETE
# SHIFT [ = ] : unpress KEY_LEFTSHIFT, press KEY_RIGHTBRACE
# CODE 1 = | : press KEY_LEFTSHIFT + KEY_BACKSLASH
# CODE 9 = { : press KEY_LEFTSHIFT + KEY_LEFTBRACE
# CODE 0 = } : press KEY_LEFTSHIFT + KEY_RIGHTBRACE
#
# Regular CODE key map for simple substitutions:
#
# CODE / = \ : press KEY_BACKSLASH
# CODE F5 = F9 : press KEY_F9
# CODE F6 = F10 : press KEY_F10
# CODE F7 = F11 : press KEY_F11
# CODE F8 = F12 : press KEY_F12
#logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
#logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
#logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.CRITICAL, format='%(asctime)s - %(levelname)s - %(message)s')
ui = UInput(name = "Tandy 102 Keyboard", vendor = 0x01, product = 0x01)
# Tandy 102 Keyboard pin to GPIO pin map
# 1 : 11, 10 : 23,
# 2 : 12, 11 : 29,
# 3 : 13, 12 : 31,
# 4 : 15, 13 : 32,
# 5 : 16, 14 : 33,
# 6 : 18, 15 : 35,
# 7 : 19, 16 : 36,
# 8 : 21, 17 : 37,
# 9 : 22, 18 : 0
cols = [11,12,13,15,16,18,19,21,22]
rows = [23,29,31,32,33,35,36,37]
keymap = [
e.KEY_Z, e.KEY_A, e.KEY_Q, e.KEY_O, e.KEY_1, e.KEY_9, e.KEY_SPACE, e.KEY_F5, e.KEY_LEFTSHIFT,
e.KEY_X, e.KEY_S, e.KEY_W, e.KEY_P, e.KEY_2, e.KEY_0, e.KEY_BACKSPACE, e.KEY_F6, e.KEY_LEFTCTRL,
e.KEY_C, e.KEY_D, e.KEY_E, e.KEY_LEFTBRACE, e.KEY_3, e.KEY_MINUS, e.KEY_TAB, e.KEY_F7, e.KEY_LEFTALT,
e.KEY_V, e.KEY_F, e.KEY_R, e.KEY_SEMICOLON, e.KEY_4, e.KEY_EQUAL, e.KEY_ESC, e.KEY_F8, e.KEY_FN,
e.KEY_B, e.KEY_G, e.KEY_T, e.KEY_APOSTROPHE, e.KEY_5, e.KEY_LEFT, e.KEY_F2, e.KEY_GRAVE, e.KEY_NUMLOCK,
e.KEY_N, e.KEY_H, e.KEY_Y, e.KEY_COMMA, e.KEY_6, e.KEY_DOWN, e.KEY_F3, e.KEY_COPY, e.KEY_CAPSLOCK,
e.KEY_M, e.KEY_J, e.KEY_U, e.KEY_DOT, e.KEY_7, e.KEY_UP, e.KEY_F4, e.KEY_CLEAR, e.KEY_RESERVED,
e.KEY_L, e.KEY_K, e.KEY_I, e.KEY_SLASH, e.KEY_8, e.KEY_RIGHT, e.KEY_ENTER, e.KEY_PAUSE, e.KEY_F1,
]
# NUM LOCK keymap: M=0, J=1, K=2, L=3, U=4, I=5, O=6
numlock_keymap = [
e.KEY_Z, e.KEY_A, e.KEY_Q, e.KEY_6, e.KEY_1, e.KEY_9, e.KEY_SPACE, e.KEY_F5, e.KEY_LEFTSHIFT,
e.KEY_X, e.KEY_S, e.KEY_W, e.KEY_P, e.KEY_2, e.KEY_0, e.KEY_BACKSPACE, e.KEY_F6, e.KEY_LEFTCTRL,
e.KEY_C, e.KEY_D, e.KEY_E, e.KEY_LEFTBRACE, e.KEY_3, e.KEY_MINUS, e.KEY_TAB, e.KEY_F7, e.KEY_LEFTALT,
e.KEY_V, e.KEY_F, e.KEY_R, e.KEY_SEMICOLON, e.KEY_4, e.KEY_EQUAL, e.KEY_ESC, e.KEY_F8, e.KEY_FN,
e.KEY_B, e.KEY_G, e.KEY_T, e.KEY_APOSTROPHE, e.KEY_5, e.KEY_LEFT, e.KEY_F2, e.KEY_GRAVE, e.KEY_NUMLOCK,
e.KEY_N, e.KEY_H, e.KEY_Y, e.KEY_COMMA, e.KEY_6, e.KEY_DOWN, e.KEY_F3, e.KEY_COPY, e.KEY_CAPSLOCK,
e.KEY_0, e.KEY_1, e.KEY_4, e.KEY_DOT, e.KEY_7, e.KEY_UP, e.KEY_F4, e.KEY_CLEAR, e.KEY_RESERVED,
e.KEY_3, e.KEY_2, e.KEY_5, e.KEY_SLASH, e.KEY_8, e.KEY_RIGHT, e.KEY_ENTER, e.KEY_PAUSE, e.KEY_F1,
]
# CODE keymap: /=\, F5=F9, F6=F10, F7=F11, F8=F12
code_keymap = [
e.KEY_Z, e.KEY_A, e.KEY_Q, e.KEY_O, e.KEY_1, e.KEY_9, e.KEY_SPACE, e.KEY_F9, e.KEY_LEFTSHIFT,
e.KEY_X, e.KEY_S, e.KEY_W, e.KEY_P, e.KEY_2, e.KEY_0, e.KEY_BACKSPACE, e.KEY_F10, e.KEY_LEFTCTRL,
e.KEY_C, e.KEY_D, e.KEY_E, e.KEY_LEFTBRACE, e.KEY_3, e.KEY_MINUS, e.KEY_TAB, e.KEY_F11, e.KEY_LEFTALT,
e.KEY_V, e.KEY_F, e.KEY_R, e.KEY_SEMICOLON, e.KEY_4, e.KEY_EQUAL, e.KEY_ESC, e.KEY_F12, e.KEY_FN,
e.KEY_B, e.KEY_G, e.KEY_T, e.KEY_APOSTROPHE, e.KEY_5, e.KEY_LEFT, e.KEY_F2, e.KEY_GRAVE, e.KEY_NUMLOCK,
e.KEY_N, e.KEY_H, e.KEY_Y, e.KEY_COMMA, e.KEY_6, e.KEY_DOWN, e.KEY_F3, e.KEY_COPY, e.KEY_CAPSLOCK,
e.KEY_M, e.KEY_J, e.KEY_U, e.KEY_DOT, e.KEY_7, e.KEY_UP, e.KEY_F4, e.KEY_CLEAR, e.KEY_RESERVED,
e.KEY_L, e.KEY_K, e.KEY_I, e.KEY_BACKSLASH, e.KEY_8, e.KEY_RIGHT, e.KEY_ENTER, e.KEY_PAUSE, e.KEY_F1,
]
GPIO.setmode(GPIO.BOARD)
for row in rows:
logging.debug(f"Setting pin {row} as an output")
GPIO.setup(row, GPIO.OUT)
for col in cols:
logging.debug(f"Setting pin {col} as an input")
GPIO.setup(col, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
# Set object to store pressed keys
pressed = set()
# Normal polling rate
sleep_time = 1/60
# Keep track of how many keyboard polls since the last keypress
polls_since_press = 0
# Keep track of the numlock state, default to off
num_lock = 0
# SHIFT/CODE/NUM-LOCK key scan values
SHIFT_KEY = 8
CODE_KEY = 35
NUMLOCK_KEY = 44
# Keep track of keys modified with SHIFT and CODE keys
shifted_key = 0
coded_key = 0
while True:
sleep(sleep_time)
syn = False
for i in range(len(rows)):
logging.debug(f"Setting row {i} high, pin {rows[i]}")
GPIO.output(rows[i], GPIO.HIGH)
for j in range(len(cols)):
# Look up the keycode in our map
keycode = i * (len(rows) + 1) + j
logging.debug(f"Checking column {j}, pin {cols[j]} which results in key {keymap[keycode]}")
newval = GPIO.input(cols[j]) == GPIO.HIGH
# ========================================================================================================================
# Detect a newly pressed key (Is our pressed key not yet in the set of pressed keys?)
# ========================================================================================================================
if newval and not keycode in pressed:
# Add it to the set
pressed.add(keycode)
# ---------------------------------------------------------
# NUM-LOCK handler
# ---------------------------------------------------------
if keycode == NUMLOCK_KEY:
if num_lock == 0:
num_lock = 1
logging.info(f"Pressed {keycode} - Set num_lock = 1")
else:
num_lock = 0
logging.info(f"Pressed {keycode} - Set num_lock = 0")
# ---------------------------------------------------------
# Handling for SHIFT BS, SHIFT [, and CODE modifiers
# ---------------------------------------------------------
# SHIFT BS - Generate DEL
if keycode == 15 and SHIFT_KEY in pressed:
# Release the SHIFT key and send DEL instead
logging.info(f"Pressed {keycode} but actually press e.KEY_DELETE instead due to SHIFT key")
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
ui.write(e.EV_KEY, e.KEY_DELETE, 1)
shifted_key = e.KEY_DELETE
# SHIFT [ - Generate right brace
elif keycode == 21 and SHIFT_KEY in pressed:
# Release the SHIFT key and send right brace instead
logging.info(f"Pressed {keycode} but actually press e.KEY_RIGHTBRACE instead due to SHIFT key")
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
ui.write(e.EV_KEY, e.KEY_RIGHTBRACE, 1)
shifted_key = e.KEY_RIGHTBRACE
# CODE 1 - Generate verticle bar
elif keycode == 4 and CODE_KEY in pressed:
# Send SHIFT \ to get a |
logging.info(f"Pressed {keycode} but actually send e.KEY_LEFTSHIFT + e.KEY_BACKSLASH instead due to CODE key")
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 1)
ui.write(e.EV_KEY, e.KEY_BACKSLASH, 1)
coded_key = e.KEY_BACKSLASH
# CODE 9 - Generate Left curly brace
elif keycode == 5 and CODE_KEY in pressed:
# Send SHIFT [ to get a {
logging.info(f"Pressed {keycode} but actually send e.KEY_LEFTSHIFT + e.KEY_LEFTBRACE instead due to CODE key")
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 1)
ui.write(e.EV_KEY, e.KEY_LEFTBRACE, 1)
coded_key = e.KEY_LEFTBRACE
# CODE 0 - Generate Right curly brace
elif keycode == 14 and CODE_KEY in pressed:
# Send SHIFT ] to get a }
logging.info(f"Pressed {keycode} but actually send e.KEY_LEFTSHIFT + e.KEY_RIGHTBRACE instead due to CODE key")
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 1)
ui.write(e.EV_KEY, e.KEY_RIGHTBRACE, 1)
coded_key = e.KEY_RIGHTBRACE
# -------------------------------------------------------------------
# Regular handler using keymap[], numlock_keymap[], and code_keymap[]
# -------------------------------------------------------------------
else:
# Check for CODE being held down; if so use code_keymap
if CODE_KEY in pressed:
logging.info(f"Pressed {keycode} which is CODE-key {e.KEY[code_keymap[keycode]]} Column {i} Row {j}")
ui.write(e.EV_KEY, code_keymap[keycode], 1)
coded_key = code_keymap[keycode]
# Check for num-lock being set; if so use numlock_keymap
elif num_lock:
logging.info(f"Pressed {keycode} which is num-locked key {e.KEY[numlock_keymap[keycode]]} Column {i} Row {j}")
ui.write(e.EV_KEY, numlock_keymap[keycode], 1)
# Otherwise use regular keymap
else:
logging.info(f"Pressed {keycode} which is key {e.KEY[keymap[keycode]]} Column {i} Row {j}")
ui.write(e.EV_KEY, keymap[keycode], 1)
syn = True
# ========================================================================================================================
# Detect if the key is released (If there was a state change, was our pressed key in the set of pressed keys?)
# ========================================================================================================================
elif not newval and keycode in pressed:
# Record the released key state to the system - process all exceptions
if keycode == 15 and SHIFT_KEY in pressed:
logging.info(f"Released {keycode} but actually release e.KEY_DELETE due to SHIFT key")
ui.write(e.EV_KEY, e.KEY_DELETE, 0)
elif keycode == 21 and SHIFT_KEY in pressed:
logging.info(f"Released {keycode} but actually release e.KEY_RIGHTBRACE due to SHIFT key")
ui.write(e.EV_KEY, e.KEY_RIGHTBRACE, 0)
elif keycode == 4 and CODE_KEY in pressed:
logging.info(f"Released {keycode} but actually release e.KEY_LEFTSHIFT + e.KEY_BACKSLASH instead due to CODE key")
ui.write(e.EV_KEY, e.KEY_BACKSLASH, 0)
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
elif keycode == 5 and CODE_KEY in pressed:
logging.info(f"Released {keycode} but actually release e.KEY_LEFTSHIFT + e.KEY_LEFTBRACE instead due to CODE key")
ui.write(e.EV_KEY, e.KEY_LEFTBRACE, 0)
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
elif keycode == 14 and CODE_KEY in pressed:
logging.info(f"Released {keycode} but actually release e.KEY_LEFTSHIFT + e.KEY_RIGHTBRACE instead due to CODE key")
ui.write(e.EV_KEY, e.KEY_RIGHTBRACE, 0)
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
# -------------------------------------------------------------------
# Regular handler using keymap[], numlock_keymap[], and code_keymap[]
# -------------------------------------------------------------------
else:
# Check for CODE key being held down; if so use code_keymap for the release event
if CODE_KEY in pressed:
logging.info(f"Released {keycode} which is CODE-key key {e.KEY[numlock_keymap[keycode]]}")
ui.write(e.EV_KEY, code_keymap[keycode], 0)
# Check for num-lock being set; if so use numlock_keymap for the release event
elif num_lock:
logging.info(f"Released {keycode} which is num-locked key {e.KEY[numlock_keymap[keycode]]}")
ui.write(e.EV_KEY, numlock_keymap[keycode], 0)
# Otherwise use regular keymap for the release event
else:
logging.info(f"Released {keycode} which is {e.KEY[keymap[keycode]]}")
ui.write(e.EV_KEY, keymap[keycode], 0)
# If CODE was released while another is still being held down:
# 1. Release this extra key to prevent continuous key repeat
# 2. Release the shift key
if keycode == CODE_KEY and len(pressed)>1:
logging.info(f"Also releasing {coded_key} and SHIFT")
ui.write(e.EV_KEY, coded_key, 0)
ui.write(e.EV_KEY, e.KEY_LEFTSHIFT, 0)
# If SHIFT was released while another is still being held down, also release this extra key to prevent continuous key repeat
if keycode == SHIFT_KEY and len(pressed)>1:
logging.info(f"Also releasing {shifted_key}")
ui.write(e.EV_KEY, shifted_key, 0)
# Remove it from the set
pressed.discard(keycode)
syn = True
GPIO.output(rows[i], GPIO.LOW)
if syn:
ui.syn()
polls_since_press = 0
sleep_time = 1/60
else:
polls_since_press = polls_since_press + 1
if polls_since_press == 600:
logging.info(f"Reducing polling rate")
sleep_time = 1/10
elif polls_since_press == 1200:
logging.info(f"Reducing polling rate again")
sleep_time = 1/5
To make it operational, I added the following line to /etc/rc.local:
python3 /etc/keyboard.py &
That tells Armbian to launch keyboard.py during boot, and keep it running as a background process due to the “&” at the end. It is already running when the Cinnamon desktop presents the login prompt. Given all of the extra keys that I’ve implemented I really shouldn’t ever have a reason to attach a USB keyboard to the PINE A64 at this point.
Here’s my first moment of success with a fully operational keyboard driver: