When resurrecting any antique micro the electronic parts are often easiest to replace with modern parts with faster speeds or bigger memory size but the keyboard is not. Individual switches might cost almost a pound each, and when multiplied by the sixty or so keys required can become costly enough to discourage the whole project.
A PC keyboard can now be bought for a few pounds, but has its own serial interface.
One solution might be this arrangement:
| PC kbd |
-serial data-> | PS/2 interface |
-scan codes-> | Scan-code to simulated matrix converter |
--> | Antique micro |
This has been done in various ways before - e.g. having a PIC micro between the PC keyboard and the antique micro. Such interfaces are target specific.
Programmable logic chips are becoming more accessible, and thus more attractive for cloning antique micros. I searched for code related to PS/2 keyboard interfacing, and quickly found some. Once developed, this would be the same for all modern PC keyboards, and only the key-matrix adapter would be target-specific.
I don't actually have a ZX81 at the moment, so I cannot develop a ZX81 keyboard in isolation. I did have some Acorn Atoms that have keyboards with old corroded key contacts, so these would benefit from simulated keyboards. This is a problem for which I had been looking for a solution anyway.
I have collected details of the keyboard matrices for:
the ZX81, the Acorn Atom, the Atari 800XL, the BBC Micro, the TRS80, the CoCo, the C64, and the TI89/TI92.
If anyone has any others let me know.
At opencores.org projects has code for talking to PS2 keyboards and mice. I had a look at the keyboard file and the author has put detailed notes at the beginning.
The demonstration-code runs in an XC2S200 (as per the B5 board) and halves a 49.152 MHz clock to drive an LCD panel (from a Thinkpad 700C). The four PS/2 ports are written to use the same clock. The demo code also requires a second clock running at 32MHz. There are two demos: the first converts PS/2 scan codes to serial data exchanged through a UART. The second demo has four PS/2 mice to run some sort of 'bat-and-ball' video game on the LCD.
The opencores code is slightly inconvenient in that it is Verilog, while the main body of Bodo's code is VHDL. This was deliberate done by the PS/2 interface's designer, to test the mixed-language abilities of the development software used. There is software to convert Verilog to VHDL but the latter code would need checking. A quick search of the net shows these either cost money or free but runs with some limitation. There is one that runs on a DEC in C++ but needs porting to the PC for instance.
I searched the web for a VHDL PS/2 interface and found: http://www.itee.uq.edu.au/~peters/xsvboard/
which might be a better start.
The demo code is a lot simpler than the Verilog demo, in that it simply displays keyboard codes on a couple of 7-segment displays. (See sketch).
| PC kbd |
--> | PS/2 interface |
-scan-code-> | FIFO | -> | 7-segment displays |
|
| <-- | <-control-codes- | debouncer | <-- | Switches |
The demo was written to run on a XSV board v1.0, produced by XESS Corp, so some tinkering will be required in the pinout files. But certainly this is less effort than translating the Verilog code to VHDL.
The XSV board has a Xilinx Virtex chip and a Xilinx XC95108 CPLD chip. The PS/2 interface resides in the former, and the latter seems to be used for some sort of downloading purpose.
The code includes a FIFO, which the demo reads out at a
human-readable rate of one byte per second.
This could be discarded, since computers that use matrix
keyboards have their own methods to scan and debounce the keys.
On the other hand if you have plenty of logic available then you
might like to have this as a small 'type-ahead' buffer.
I ported this code to the B5 board and there are a few points to note. Firstly the active-low reset line is not optional: it must be pulled low then high in order for the logic to work. Secondly the 7-segment displays assume a different numbering scheme to the usual convention, so the codes initially looked very alien. I modified the codes to use the same convention as Hewlett-Packard: segments a to g (or 0 to 6) are clockwise-from-top with the last segment in the middle. Finally the demo did not work quite as I expected it to. I had expected the display to respond fairly promptly to key-presses, but it seemed that I many keys could be pressed before their codes appeared on the display. This is due to the FIFO and the module that reads it.
The FIFO works like a 9-bit wide shift register, the 9th bit being a 'valid' flag. This is read every second, but if there is no valid data in an entry then the data output remains unchanged and the entries are clocked along. Thus the demo code can take up to 15 seconds to clock past the 15 invalid entries to reach a valid one. This shift register implemementation has some advantages but doesn't work quite as one would expect a FIFO to work - it has 'bubbles' in its data pipeline. The demo seems to get round this by flushing bubbles until valid data is found.
I didn't like this implementation, because all the bits in a shift-register have to shift at each clock. Bigger shift registers mean more data to shift.
Such buffers are sometimes needed in software, and can be very long. Rather than shuffle a large block of data along, such software buffers are better implemented by using pointers. These move the entry and exit points of the data instead of moving the data. This is much more efficient for both software and hardware.
Software can do this very simply by incrementing and decrementing pointers. This alternative to shift registers uses a block of RAM with input and output pointers. Writing data adjusts the former, reading data adjusts the latter. Their difference indicates the 'fullness' of the FIFO. This approach is extensible to other types of buffers (like Circular or Delay lines) and because the memory is separate from the pointers, the memory can be outside the FPGA and very large. I had used this technique in my B.Sc. final year project.
There is another optimisation we can make, by using Linear Feedback Shift Registers (LFSRs) instead of Arithmetic Counters for the pointers. These have several advantages: they use less logic than counters because they do not need the carry logic of arithmetic counters, and it is easy to generate adjacent pointer values. LFSRs progress in non-simple but regular sequences. See Xilinx App Note 044.000 for a detailed description, and their app note "Megabit FIFO using DRAM".
I modified the demo code to use such a FIFO but it still didn't work as expected. I spent a lot of time tinkering with the FIFO code but could not find fault with it. I then had a look at the reader code. Eventually I had a look at the raw data being fed to the FIFO, by connecting the 7-seg displays to the FIFO input instead of output. This showed the keyboard data pretty much as expected but interspersed with regular 0xFE bytes. I had originally thought this to be uninitialised FIFO data, but 0xFE is also the keyboard's code to ask the PC to retransmit the last byte. So perhaps the demo code was sending bytes to the keyboard that the keyboard did not understand and assumed to be misheard. So I looked at the sending part of the demo code and disabled the part that sensed the 'send data' input. This stopped the unexpected 0xFE bytes.
The demo code seems to work as expected: the FIFO 'full' and 'not empty' flags work, as do their pointers. There is still one problem left: the receiver sometimes gets out of sync, which means the data bytes start to be shifted by one or more bits. This shift may change, and sometimes back to normal, but the reset pin reliably restors correct sync behaviour. This problem needs fixing because the shifted data can be mistaken for valid scan codes.
At this point I notice the VHDL vs. Verilog flame war going on. Opinions vary, but I get the impression that Verilog and VHDL occupy similar positions as C and ADA respectively. As a C programmer, I do find VHDL looks more alien than Verilog. Sadly I suspect that FPGA designers will have to be familiar with both in the current job market. I wish human beings would design single products that come in various levels of complexity for various types of application, rather than multiple incompatible products.
A true matrix is bidirectional and an analogue cross-point switch would cope with all scanning methods (e.g. row/columns driven/sensed alternately). But if the inputs and outputs are fixed the matrix can be simulated by a dual-port RAM (driven on address lines, sensed on the data lines). I had previously thought of doing this with a micro and a real DP RAM as a generic solution for replacing the keyboard of any proprietary matrix. However, when you have a whole FPGA to play with you can integrate this into the FPGA.
The ZX81 I/O map only needs to detect eight addresses for keyboard scanning. At first sight this might imply eight 8-bit comparators, but one could easily detect the two terms where A15-12 or A11-8 == 1111. Each could then be shared by four 4-bit comparators.
The conversion is non-trivial: PS/2 keyboard codes involve multiple bytes per keypress. Some of them are quite long. Thus the interface will have to buffer these and interpret them. Alternatively one might simply state that certain keys will confuse the ZX81, which is forgivable since the long-sequence keys are ones which have no valid meaning on the ZX81. The PS/2 keyboard sequences are documented on the web.
Some people have commented that a membrane keyboard gives a truer experience. Almost all PC keyboards today use membranes internally. Thus you could produce a more ZX81-like keyboard by ripping the top of a PC keyboard off (key caps and rubber 'springs'), then pasting a printed ZX81 keyboard overlay. This can be laser-printed onto paper and covered with transparent sticky-back plastic, or directly onto plastic film that can withstand laser-printer heat. While the membrane is accessible, you could disable the troublesome keys by putting sellotape over their contacts to insulate them.
If the serial keyboard is used, then there is no keyboard matrix to read the start-up modes from. Therefore this will need to be read from dedicated input pins. This seperates the mode setting from the keyboard.
These are small, and fiddly to hand wire. Maplin sell a 6-way mini-DIN line socket (stock no. PL56) which is one solution, but the connections inside are simply pins with hardly any central hole to hold wires being soldered. It seems far easier to buy a PS/2 keyboard cable extender and use the relevant half of it. You would still have to wire the sawn-off ends somehow.
I bought a PS/2 female to 9-way D-type female adapter (Maplin part CD72), as used for connecting new PS/2 mice to old serial ports. This allows connection to a 9-way D-type male IDC plug on ribbon cable which can then connect to a 10-way 0.1" grid header. A bit convoluted, but less fiddly soldering. A quick probe with a multimeter indicates only pins 1,2,3,5 and 6 of the D-types are connected. Wishing to document the connections, I checked the web and found conflicting pin numbers for the PS/2. Worse, I find that such adapters are almost useless since they vary in the way they work - RS232 and PS/2 mice/kbds are very different things and any adapters are really just kludges.
BurchED have an adapter board that provides buzzer, keyboard (PS/2 and DIN), mouse (PS/2) and VGA connectors. The connections for these may guide the pinout of the final design. The VGA connector may not be useful driving a VGA (due to frame rates etc) but may well be useful for connecting an LCD.
The B5's predecessor (the B3) has to have its PC adapter board on particular I/O connectors so that the clock signal is delivered to the right pin. Although the B5 has its own clock source, it might be cautious to choose the same I/O connectors so that the design will be most portable between the two boards.
The B5 and B3 use different adapter boards: the B3 version had a header-programmable oscillator, the B5 version does not because the B5 already has one.
Most key codes are single bytes.
Codes with D7 low are single-byte scan codes.
Codes with D7 high are code modifiers.
Code 0xF0 indicates the next code is of a key being released.
Code 0xE0 indicates the next code is extended
| Hex | Normal | After E0 | Comments |
| 14 | Left Ctrl | Right Ctrl | Alternative key positions |
| 11 | Left Alt | Right Alt | |
| 4a | Main / | CalcPad / | |
| 5a | Main Enter | CalcPad Enter | |
| 6c | CalcPad 7 | Home | Alternative key functions |
| 75 | CalcPad 8 | Up Arrow | |
| 7d | CalcPad 9 | Page Up | |
| 6b | CalcPad 4 | Left Arrow | |
| 74 | CalcPad 6 | Right Arrow | |
| 69 | CalcPad 1 | End | |
| 72 | CalcPad 2 | Dn Arrrow | |
| 7a | CalcPad 3 | Page Dn | |
| 70 | CalcPad 0 | Ins | |
| 71 | CalcPad . | Del |
So for most keys, coding is single byte with an 0xF0 prefix if
rising.
The 0xE0 prefix byte merely extends the code to be a key that is
either identical but at an alternative location, or on the same
key but an an alternate function.
The long-sequence codes are :
Print Screen/SysRq = e0 12 e0 7c = extended-shift,
extended-calcpad star.
and
Pause/Break = e1 14 77 e1 f0 14 f0 77 == extended-ctrl, numlock,
extended-release-ctrl, release-numlock.
Logic circuits need to detect 0xF0 to know whether a virtual key should be released or not, and 0xE0 to select alternative key functions.
So you design to the spec, and it still doesn't work. I
checked this out, and it seems that anyone who has made a real
interface has added a lot of signal filtering. The keyboard
signals are appallingly bad!
Hamblen and Furman's book samples the signals at 25 MHz. If the
signal is the same value 8 times in a row, i.e. 0.32 µs, it is
considered to have settled.
I added the filtering so that the signal has to be consistent for 8 samples at 1 MHz. Quite long, but the timing periods are around 60 microseconds so still acceptable. This did not fix it. Pull-ups too weak? I measured the pull-ups on the mouse port of an old motherboard mouse port and found they were 1K. I reduced my pull-ups from 4k7 to 1K and eventually 470R. Still not fixed.
Crosstalk perhaps? Moving the signals to well-seperated pins didn't fix it either. Perhaps the crosstalk was from the few inches of ribbon cable I had used? I cut open some keyboard cable and found that the wiring in there had no cross-talk countermeasures such as twisted pairs or seperate shielding. These cables go over a metre so the short bit of ribbon cable I had was not significant. There's not much point in taking much care with ps2 wiring because the keyboard cables themselves do not.
Someone suggested that pull-ups should be chosen to prevent ringing, i.e. they should match the impedance of the wires at about 220R. That's about 25 mA each signal for the keyboard to sink, close to the limit for an I/O port.
On the old board I noticed there were surface-mounted series inductors on the signals (marked 331 for 330 µH I think) and the VCC outputs (marked 222 for 2200 µH I think). These are known to counter ringing.
Before I proceed any further I'd like to get a look at the signals on a scope, to see just how bad they are and in what way. It may be that occasional errors are unavoidable and have to be recovered from. The example code I have does not recover. One mechanism might be to assume that if both signals are undriven for longer than say 1000 µs it should abort and return to the 'idle' state.