The project can be split into two parts:
- Teensy as a host for a standard USB keyboard.
This is well supported by Teensyduino: you can use Teensy 4.1 or 3.6 (easiest, with USB host port exposed on pins, so you only need to plug in a suitable cable), or Teensy 4.0 (soldering an USB female connector to the pads underneath the board).
- Using 8 input and 8 output pins to interface to the TRS-80 8x8 scanning matrix.
If I understood correctly, the TRS-80 keyboard matrix has 8 outputs (row selectors), and 8 inputs (columns), with each tactile switch connecting one output to one input when pressed, normally open; and running at 5 V logic. Because Teensy uses 3.3 V logic, you do need 8+8 voltage level shifters. I've found auto direction sensing to be problematic here (glitches in the low level signal due to direction mis-sensing), so I would definitely recommend unidirectional shifter here, say SN74LVC8T245's (8 signals per chip).
One could also use say 16 NX138AKR (N-channel signal MOSFETs in SOT-23 footprint; my current favourite, since the gate can handle ±20 V, but fully switches with Teensy's 3.3V logic levels), but the configuration depends on whether the matrix row selectors are active high or active low. (These cost 0.13€ apiece at mouser, so 16 costs about 2.10€. I bought a strip of 100 for 6€. You also need 16 pull-up resistors of 2.2k-10k, and optionally 16 100-1000 ohm resistors to limit the current spike when switching the gate state.)
The Teensy code needs to maintain an internal bit map corresponding to the 8×8 matrix. This is a simple
volatile unsigned char matrix_state[8]; whose bits are set when the corresponding keypress is received from the USB keyboard, and cleared at key releases. Note that modifier key states are reported for every keyboard USB HID event.
On the eight matrix row selector pins, you set an interrupt handler for the rising edge. In the interrupt handler, you set the eight column pins based on the matrix.
The latency on the interrupt handler should be short, so that the column matrix pins will reflect the correct state for the active row, fast enough for the matrix scanning circuitry to not notice. If you use Teensy 4.0 or 4.1, one useful 'trick' you might consider –– noting that standard
digitalWriteFast() is likely fast enough! –– is to use column pins that are in the same GPIO bank. For example, Teensy pins 14 - 21 (GPIO6 bits 18, 19, 23, 22, 17, 16, 26, and 27), via a lookup table,
uint32_t matrix_lookup[256];, which is initialized in setup():
Code:
volatile unsigned char matrix_state[8];
uint32_t matrix_lookup[256];
void setup() {
for (int i = 0; i < 8; i++) {
matrix_state[i] = 0;
}
for (unsigned int mask = 0; mask < 256; mask++) {
uint32_t value = 0;
if (mask & (1<<0)) value |= 1 << 18; // GPIO6 bit 18 = Teensy 4.x pin 14 = column 0
if (mask & (1<<1)) value |= 1 << 19; // GPIO6 bit 19 = Teensy 4.x pin 15 = column 1
if (mask & (1<<2)) value |= 1 << 23; // GPIO6 bit 23 = Teensy 4.x pin 16 = column 2
if (mask & (1<<3)) value |= 1 << 22; // GPIO6 bit 22 = Teensy 4.x pin 17 = column 3
if (mask & (1<<4)) value |= 1 << 17; // GPIO6 bit 17 = Teensy 4.x pin 18 = column 4
if (mask & (1<<5)) value |= 1 << 16; // GPIO6 bit 16 = Teensy 4.x pin 19 = column 5
if (mask & (1<<6)) value |= 1 << 26; // GPIO6 bit 26 = Teensy 4.x pin 20 = column 6
if (mask & (1<<7)) value |= 1 << 27; // GPIO6 bit 27 = Teensy 4.x pin 21 = column 7
matrix_lookup[mask] = value;
}
pinMode(14, OUTPUT);
pinMode(15, OUTPUT);
pinMode(16, OUTPUT);
pinMode(17, OUTPUT);
pinMode(18, OUTPUT);
pinMode(19, OUTPUT);
pinMode(20, OUTPUT);
pinMode(21, OUTPUT);
GPIO6_DR_CLEAR = matrix_lookup[255];
}
The
matrix_state array is marked
volatile, to tell the compiler it cannot cache or infer the value it should have based on the code it sees. I am assuming an interrupt handler may read or modify its contents, and as that interrupts the normal program flow in unpredictable ways, the compiler must not cache or infer the value from normal program flow, and must actually examine the contents whenever the code refers to the array.
Assuming the row selectors are active high, any row selector going low should clear all the matrix outputs, ie.
GPIO6_DR_CLEAR = matrix_lookup[255];.
When the row selector for row
row (0 to 7) goes high, you do
GPIO6_DR_CLEAR = matrix_lookup[(~matrix_state[row]) & 255]; GPIO6_DR_SET = matrix_lookup[matrix_state[row] & 255]; to set the matrix outputs to correspond to the states on that row in
matrix_state.
This is compatible with
digitalWriteFast(), even with these same pins or pins in the same GPIO bank. The clear pulls the columns corresponding to inactive matrix bits low first, so that the likelihood of "ghosting" (matrix scanner seeing the state for the previous row) is minimised; and only then sets the columns corresponding to active matrix bits.
If the scanner checks the column bits faster than Teensy can set them, this way the key press is only delayed by one scan cycle.
If we don't clear the column bits fast enough, then a key press can occasionally "ghost", i.e. as if the same line on the next row is also active.
(There should be a reliable delay, though, because these signal lines have inherent capacitance; even the real hardware may "ghost" the same way if the delay between enabling the specific row and reading the column lines is not long enough. As typical rates for a 8-row matrix are between 400 Hz (2.5ms per row, 50 Hz key state check rate) and 2000 Hz (0.5ms per row, 250 Hz key state check rate), there should be at least 0.1 ms between the activation of a row, and the TRS-80 checking the column states. For Teensy 4.x, that is
a long time; you can do a lot of stuff in 0.1 ms = 100 µs in Teensy 4.x.)
If you are or become proficient with Teensyduino programming, you can then replace the IRQ_GPIO6789 interrupt handler –– which handles
all GPIO pin interrupts –– with one that checks the eight row selector pins, and sets the matrix outputs as above. (If exactly one row selector is active, that determines the row; otherwise, clear all outputs. Also, if the row selectors are in the same bank, and
mask contains the states for the row selector bits,
(mask && (mask & (mask - 1))) is true if and only if exactly one bit in
mask is set; this is a trick one can use to speed up the cases where you support other pin state interrupts, but check the matrix row selectors first.)
This way you can trade some functionality (namely, other GPIO pin interrupt support) for a very fast interrupt handler, so that when Teensy 4.x runs at 600 MHz, the reaction delay is minimized to on the order of a single microsecond (1 µs = 0.001 ms), which should ensure a very robust keyboard matrix emulation, even at quite high (tens of kHz) matrix scanning rates.
This tricky stuff is definitely normally not needed for keyboards, but can be useful for implementing parallel buses, i.e. using Teensy to emulate some old 8-bit peripheral or perhaps even memory (but with a several MHz parallel 8-bit buses, I suspect one would need to do some of the stuff in extended inline assembly, sprinkling assembly functions for the critical bus data switching parts, but using the same above approach). If the output pins are not set by any other code, then the update can be made with a single
oldmask ^= matrix_lookup[matrix_state[row]]; GPIOn_DR_TOGGLE = oldmask;, which simply tracks the pins that
change (using global/static
oldmask), toggling the state of the pins instead of explicitly setting and clearing them.
So, do consider most of this message as rambling.