There are many different ways to build a keyboard for a homebrew computer.
Some are satisfied with a retro 20-key hexpad for interacting with debug monitors. Some prefer to interface somehow with a modern PC keyboard, despite the frankly absurd contortions of the PS/2 and USB-HID protocols. Other options would include essentially copying the
BBC Micro's 73-key design, which interrupts the CPU when a key is pressed, but then the CPU has to scan for and debounce that key by itself. Or a microcontroller could be pressed into service to do that part, leaving only the problems of interfacing to the main CPU, and of programming that microcontroller in the first place.
I'm taking a different tack. I'm going to do the scanning and debouncing entirely in discrete hardware. It looks like it's going to need a couple of dozen ICs, only two of which should cost more than €2 each, and a handful of passive components. By far the biggest costs will be for the keyswitches and PCB, though. With that in mind, accepting sub-par performance for the sake of a few dollars in part count just isn't on. I've taken the
Danluu Report into consideration, looked into Cherry's RealKey tech and found it unnecessarily fragile in design (and frankly unambitious in performance), and I'm pretty sure I can do better.
So I'm aiming for sub-millisecond key-down latency, full N-key rollover, and support for a 96-key layout (plenty to support a PC-XT or "
pocket computer" type layout). The CPU will be interrupted on both key-down and key-up events, and then it only has to read a scancode from a FIFO and use it to update a bitmap in memory, from whence converting it to ASCII or whatever is straightforward and quick. Should that bitmap need to be refreshed from scratch, no problem, just reset the keyboard hardware, and the current set of pressed keys will then be scanned and presented as key-down events. With any luck, power consumption should also be respectably low, especially when no keys are pressed.
How do I achieve all that without driving myself crazy?
For a start, I do it without involving a microcontroller's finicky vendor toolchains and unknown programming hardware requirements. There is no firmware to install or debug here. So I only need to satisfy myself that the hardware logic is sound. This is the sort of thing I can reasonably verify on a breadboard before ordering the PCB, too. Besides that, I break down the design into three modules: the key-matrix, the debouncer, and the CPU interface.
Starting with that last item first, the keyboard only requires 1 byte of I/O space. Writes go to a configuration register (just a tristate octal D flipflop) which contains the debounce counter value - I can thus tune the keyboard's performance after assembly - and a reset flag. Setting that flag - which is also set by the global reset line - simply causes the keymatrix to read all keys as released while scanning, and also forces the FIFO to the empty state (so any key-up events produced will go into the bit-bucket). The FIFO, a 256x9b device of which one bit of width goes unused, is what services reads from the I/O port, and its "empty flag" produces the /IRQ signal for the device. The scancode itself is 7 bits, and the MSB is used to distinguish between key-down and key-up events.
The keymatrix is fairly conventional for an N-key rollover compatible design. This requires that a diode is placed in series with each individual keyswitch, so that if multiple keys are pressed at once, there is no ambiguity caused by backfeeding along some other row or column line common to them. I've chosen a 6-row by 16-column matrix (ie. 96 key positions), with the columns driven by a pair of 74HC238s, and outputs on the rows sampled through an RC filter each (eliminating microsecond glitches from EMI) to a hex Schmitt-trigger inverter and inputs 2-7 on a 74HC151 multiplexer. Inputs 0 and 1 are left unused, partly because that's convenient for the hex-package of Schmitt-triggers, and partly because that allows more time for an RC filter to settle after a new column is selected.
Why an active-high '238 instead of the more commonly useful active-low '138? Because the Schmitt-trigger inverters have asymmetric input thresholds that are roughly TTL compatible (yes, even in the 74HC family), so pulling a row-line down through a keyswitch and diode makes the RC filter harder to design. Pulling it up through the keyswitch & diode, and down through a resistor, is much easier to design for. Another design constraint is on the total current that all six row lines can draw on the single selected column if all six keys are pressed - I was able to make this less than 1mA. Mind you, another way of avoiding this problem would have been to insert the RC filter between two Schmitt-trigger inverter stages, but I like having been able to do it this way, and thus ballast the long lines across the PCB.
I could fit an individual diode beside each individual keyswitch, but it turns out that as well as resistor packs for bus termination etc, you can get
diode packs. A relevant part number is 74S1053, which appears to hail from the days of bipolar logic, but TI still makes them! I even get to choose whether I want common-anode or common-cathode in the same part, and I just have to make sure the unused common terminals are tied off to whichever power rail keeps them reverse-biased. They cost more than individual diodes, but will save a lot of assembly time and mistakes.
This leaves a 7-bit scancode which is simply an input to the keymatrix, while the detected state of the selected keyswitch appears at the (complementary) output of the '151. The scancode is produced by the high-order bits of a free-running 12-bit counter (74HC4040) which is itself clocked by an oscillator sourced from the main CPU board. (Not necessarily Phi2, could be independent or simply come from a different stage of a frequency divider. If I plugged this into the 6502 Fake Finder, it would be a 1.8MHz clock intended for a UART, while the CPU itself runs at 460kHz.) For design purposes I'm assuming this clock is 8MHz, which results in a complete scan time of around 0.5ms, a column scan rate of just under 32kHz, and a key scan rate of 250kHz; the system should still work properly at slower clocks, but going significantly faster would run into the RC time constant on the row filters, which has to reliably flip a Schmitt trigger (in either direction) in about two key scan times.
Incidentally, 96 keys are sufficient to directly encode every ASCII character, ignoring control keys such as Return and Tab. Most keyboards combine two characters on each key, eg. upper and lower case, or a numeral and punctuation.
The debounce strategy is to treat any detected closed keyswitch as an immediate key-down event, unless the key was already considered pressed. The RC filters on each row give enough confidence in signal integrity to trigger key-down on that basis, even before the switch has stopped bouncing on make, and this minimises what is probably the easiest form of latency to measure or notice. Open keyswitches are debounced by requiring them to remain open for a configurable number of complete scan cycles before a key-up event is generated. Cherry MX is specified to finish bouncing within 5ms, which is ten scan cycles; some other types of switch are worse, and would thus require more bits to represent appropriate debounce timeouts.
To implement the key-up debouncing and filtering out of multiple key-down events for the same keypress, the scancode also indexes into an SRAM chip. I looked into many different permutations of memory technology to find out what the cheapest and simplest way to store 96 words of just a few bits each was - and it turns out that an ordinary 55ns 8Kx8b SRAM is it. Complete overkill, technically, but it will do the job. This forms the core of a two-phase state machine, such that the first phase loads the current value from RAM into a register (attached to which is a decrementer), and the second phase implements the following behaviour:
Code:
RAM state | RAM counter | Decremented | Keyswitch | New RAM | FIFO load
----------+-------------+-------------+-----------+-----------+----------
Released | Zero | | Released | Zero | No
Released | Non-Zero | Non-Zero | Released | Decrement | No
Released | Non-Zero | Zero | Released | Decrement | Key-Up
Pressed | | | Released | Load Reg | No
Pressed | | | Pressed | Pressed | No
Released | Non-Zero | | Pressed | Pressed | No
Released | Zero | | Pressed | Pressed | Key-Down
(Blank fields mean "don't care".) Using one bit of the eight to store a "was pressed" flag leaves seven to implement the counter, enough for up to 63.5ms of debounce delay on key-up. Much slower than that would potentially leave the user able to physically restrike the key within the debounce timeout, which would leave the computer blind to the fact it had been released at all!
One shortcoming of the above design is that the CPU can't
directly query the keymatrix to discover whether a particular key is held down. But the CPU will maintain a bitmap of pressed keys which it can query, and as noted above, if that bitmap is corrupted there is a procedure to regenerate it reliably. The system reset will also cause keys held down during boot to be presented as key-down events, as long as the system reset is itself longer than the debounce delay.
It is technically possible for the FIFO to fill up, if the CPU doesn't service it faster than key events arrive. Since in that case it just ignores new data loads, I think I can just ignore the possibility. It just means it's possible for key-up events to arrive for keys that the CPU thinks weren't pressed in the first place, which is almost certainly harmless, or for key-up events to be missed for keys that were actually pressed - which will be obvious from system behaviour (if typematic repeats are implemented), and easily corrected by the user by pressing that key again.
And there is scope to extend the design beyond a mere keymatrix, due to the two rows of unused scancodes. These could be used to implement scroll wheels, depositing their events into the same FIFO as the keyboard. That's left to future work…