Couch To 64k Part 4: Adding a Keypad/Keyboard to our Z80 Breadboard Computer

Reading keys from the RC2014 keyboard

In the previous part of this series we added a character LCD display to our breadboard computer. We discussed how input, output and address decoding works on the Z80, which means we know most of what we need to know to be able to add an input device.

In this part we’ll be adding some circuitry to enable us to add either a keypad or a basic keyboard.

But before we do that there’s one more upgrade I want to make…

Additional Parts Needed

  • 4MHz Crystal Oscillator in a DIP-14 package
  • 74LS273 8-bit Latch
  • 74LS244 8-bit buffer
  • 74LS32 quad OR gate
  • Keypad or matrix keyboard
  • A few 1N4148 diodes
  • A few 10k resistors
  • A few 0.01uF capacitors

Warp Speed

So far we’ve been running our computer using a clock signal generated by the Arduino. It has been useful while testing to be able to see LEDs flashing, but now is the time run our Z80 at the kinds of speeds it was destined to run at.

In the 70s and 80s processors where clocked by a crystal (resonator). A crystal requires a few external components to generate a usable clock signal. Today you can use a TTL crystal oscillator – which includes the crystal and other parts needed to generate a TTL clock signal inside a single package. This is a quick and economical solution which is also convenient for use on a breadboard.

DIP-14 Crystal Oscillator Diagram
DIP-14 Crystal Oscillator Diagram

The crystal oscillator we’re using has four pins, one for 5V input, one for ground, and one for the output signal, and the final one is an enable signal which tri-states1 the output.

There’s space for the oscillator next to the processor. Wire up as shown in the photo with ground, earth and 5V connected as shown. The enable is asserted if not connected, so there’s no need to connect it. I’m connecting the old Arduino clock line to a spare breadboard row and allowing a little extra wire for the clock line to the Z80 so I can still switch to the slow clock if needed.

Crystal oscillator installation.
Crystal oscillator installation.

Fire this up and things should still work nicely, but bear in mind what I said last time about signals being sent to the LCD too quickly and having to add code to wait until it’s ready. Here is an example with the delay code added – see the repository.

;Pt4.1LCDFast.z80

;Constants
lcd_command equ $00     ;LCD command I/O port
lcd_data equ $01        ;LCD data I/O port
    
org 0
    ld hl,commands      ;Address of command list, $ff terminated
    
command_loop:
lcd_wait_loop1:         ;Loop back here if LCD is busy
    in a,(lcd_command)  ;Read the status into A
    rlca                ;Rotate A left, bit 7 moves into the carry flag
    jr c,lcd_wait_loop1 ;Loop back if the carry flag is set

    ld a,(hl)           ;Next command
    inc a               ;Add 1 so we can test for $ff...
    jr z,command_end    ;...by testing for zero
    dec a               ;Restore the actual value
    out (lcd_command),a ;Output it.
    
    inc hl              ;Next command
    jr command_loop     ;Repeat

command_end:                
    ld hl,message       ;Message address (ASCIIZ)
message_loop:           ;Loop back here for next character
lcd_wait_loop2:         ;Loop back here if LCD is busy
    in a,(lcd_command)  ;Read the status into A
    rlca                ;Rotate A left, bit 7 moves into the carry flag
    jr c,lcd_wait_loop2 ;Loop back if the carry flag is set

    ld a,(hl)           ;Load character into A
    and a               ;Test for end of string (A=0)
    jr z,done

    out (lcd_data),a    ;Output the character
    inc hl              ;Point to next character (INC=increment, or add 1, to HL)
    jr message_loop     ;Loop back for next character

done:
    halt                ;Halt the processor

;Startup command sequence:
;$3f: Function set: 8-bit interface, 2-line, small font
;$0f: Display on, cursor on (I find turning the cursor on is very helpful when debugging)
;$01: Clear display
;$06: Entry mode: left to right, no shift
commands:               ;$ff terminated
    db $3f,$0f,$01,$06,$ff
    
message:
    db ">HELLO< >DOCTOR< >NAME< >CONTINUE< >YESTERDAY< >TOMORROW<",0

Decoupling capacitors

And now that we’re clocking things at high speed we need a quick word about decoupling capacitors. In a digital circuit our chips need a nice stable input voltage. Other signals can affect the voltages in traces on a PCB or in wiring or on a breadboard. To counteract this we use decoupling capacitors to smooth out the supply voltage.

To date I’ve been pretty lucky when doing this kind of stuff on breadboards but I’ve also had breadboard projects where random issues where solved by adding such capacitors. I would recommend adding 0.01uF capacitors between power and ground on each breadboard, if not next to the supply voltage input to each IC.

Reading Keyboard Matrixes2

Before we can design our hardware we need to look at how keyboard matrixes work. The photo below shows a couple of basic keypads but most keyboards work in the same way at the basic level.

Some typical keypads.
Some typical keypads.

Inside the keypad is a matrix of wires as shown in the diagram.

Layout of a typical 3x4 key matrix
Layout of a typical 3×4 key matrix

There is a key at every intersection of vertical and horizontal wires. If we consider one axis to be inputs and the other to be outputs then pressing a button connects one input to one output. We read the keyboard by turning on each input in sequence and reading which, if any, output is high. This is a process known as keyboard scanning.

In a modern keyboard all of this work is handled internally by a dedicated microcontroller. In vintage computers, especially budget ones, this is done by the main processor itself. We could add counter and decoder chips to generate the input signals but the extra circuitry isn’t needed here and would add extra cost.

The way we’re going to implement this is to add a latch to one of the Z80s output ports and a buffer to one of the Z80s input ports. The latches outputs will be connected to the keyboard’s inputs. The keyboard’s outputs will be connected to the buffer’s inputs. Our code will set the latch to drive each keyboard row in turn and read the keyboard’s state via the buffer.

That paragraph used a few terms which you may not have heard before. Let me explain.

A latch is a chip such as a 74LS273 which stores a few binary bits. This operates just like an output device as discussed last time. When the clock input on the ‘273 input transitions from low to high it reads and stores the contents of the data bus (the Dn pins). The stored data is continuously output on the Qn pins.

74LS273 Pinout
74LS273 Pinout

A buffer (in our case a 74LS244) is simply a gate which put puts to contents of the inputs (the nAn) pins onto the output pins (nYn) whenever the enable (/nG) pins are asserted. When the /nG pins are not asserted the pins are tri-stated and disconnected from the bus.

74LS244 Pinout
74LS244 Pinout

The enable (/nG) lines on the ‘244 are active low, so can take a signal straight from the 74LS138 we are using for address decoding, however, because this device is only being used for input I’m going to OR3 the signal with the /RD signal so it won’t get activated if the coder accidentally tries to write to it, as described in part 3.

The clock line of the ‘273 activates on the transition from low to high. This again could be connected directly to the ‘138 – it will activate when the enable signals end, at a point when the data is still on the data bus. But I’m going to OR this against the /WR signal so it only latches when the Z80 tries to write to it.

74LS32 Pinout
74LS32 Pinout

This also enables me to use the same port address for both input and output which can help make our code simpler.

We’re nearly done, but there’s a couple of final things to bear in mind. Consider what happens if more than one key is pressed at a time. Looking again at the keyboard matrix we see that, depending on which keys are pressed, two inputs could be connected together and, therefore, shorted out. There is a simple fix to this, we add a set of diodes to the outputs from the latches.

And we also need pull-down resistors for the inputs. If not they’ll be floating and indeterminate. We also need pull-downs on unused inputs. This ensures we get zeros on any lines which don’t correspond to a key press4.

Here’s the final schematic:

Schematics for part 4
Schematics for part 4

And here’s the board wired up to use a keypad. You’ll need which rows and which columns are connected to which keys on whatever keypad/keyboard to have. If you have a datasheet this will be easy, otherwise you will need to run a few tests with your continuity meter.

The keypad I’m using has pins which are suitable inserting straight into a breadboard. Other types may need a different connection system.

Software

And now we can move to the software side of things. In the Github repo for this article I’ve added a separate source file for each stage of the coding to test each stage of code and also verify my hardware connections are correct.

First we’ll add a label (constant) for our keyboard I/O port.

;Constants
lcd_command equ $00     ;LCD command I/O port
lcd_data equ $01        ;LCD data I/O port
keyboard_port equ $20   ;Keyboard in and out port

Raw Scanning

For I/O in this code I’m going to use a different set of I/O instructions. Previously we’ve used the out (n),A instruction which writes the value in the A register to I/O port n with a matching in A,(n) instruction. For our code today we need to write the keyboard row and immediately read back the column and we’d have to swap values in and out of the A register if using these instructions. (These instructions also hard code the port address into the software which may limit your code in other ways).

Instead I’m going to use the out (C),r and in (r),C instructions. These use the C register as the port address and any of the A,B,C,D,E,H or L registers for the data to send or receive[/efn_note]There is an obvious issue with the out (C),C instruction, but since the register is encoded as a parameter in the opcode it was easier for the designers to leave it in in the design.[/efn_note]. The in (r),C instruction also sets the flags based on the value returned which we’ll use to test if any button is pressed (i.e. if the value is non-zero).

First we’re going to cycle the output through each bit/row in turn and read back the column data into the E register. If the value is non-zero we’ll jump out of the loop to process it.

The A register contains the bit/row, RLCA moving it left for the next iteration of the loop. The D register is a counter used as an index to make the next step easier. (I’m using generic code here which cycles through all eight lines. This could be optimised if we had known specific hardware).

And once we’ve tested all rows RLCA will move the bit in the A register into the carry flag which indicates we can exit the routine. (For testing purposes this simply loops back for another scan).

keyboard_scan:
    ld c,keyboard_port  ;Port address
    ld a,1              ;Row to scan
    ld d,0              ;Row index
keyboard_loop:          ;Loop for each row
    out (c),a           ;Output row
    in e,(c)            ;Read columns
    jr nz,key_found     ;If non-zero then a key is being pressed
    
    inc d               ;Next index
    rlca                ;Next row
    jr nc,keyboard_loop ;Loop until the bit falls out

                        ;End of scan loop - nothing pressed
    jr keyboard_scan    ;Loop infinitely (because this is a test)
Raw keypad scanning. The number on the left is the row index. On the right is the raw binary column data returned for each key.

Column Index

The next step is to convert the returned column data in E into an index ready for the next step. We repeatedly rotate the E register to the right and increment a counter in A until a bit ‘pops out’ of E into the carry flag.

key_found:              ;We found a pressed key
                        ;Print row index (decimal) and column (binary)
    ;A=Row bit
    ;D=Row index
    ;E=Returned column
    xor a               ;A=0 (index counter)
column_index_loop:
    rr e                ;Rotate E right until we find the bit which is set
    jr c,column_index_done
    inc a               ;Otherwise, inc index and loop again
    jr column_index_loop
Converting column scan data to an index. On the left is the row index (as before) on the right is the column index.

Key Index

Now we combine both values together into the A register. I adjust the A register because I’m not using every row in the hardware. I then shift the value left with a pair of add a,a instructions and add the row index (D register) to get the final key index.

column_index_done:
    ;A=Column index
    ;D=Row index
    ;Calculate key number (index into look up table)
    ;We'll end up with a value, in A, 0000ccrr where cc is the column index
    ;and rr is the row index.
    ld e,a              ;Save column into E
    
    ld a,-4             ;Our keypad only uses the low 4 bits of the row
    add a,d             ;address so we need to subtract 4 from the index.
                        ;We do this by adding -4 to keep the code shorter.
                        ;We could simplify this with known specific 
                        ;hardware.
                        
    add a,a             ;Shift column left 2 bits. Adding a number to itself
    add a,a             ;is the same as a left shift but ADD A,A is faster 
                        ;on Z80.
    
    add a,e             ;Add on row. A = final key value.

Converting to a Character

And now that we have a key value we can find the actual character in a lookup table. Our lookup table is safely out of the way at the end of the file. This one can read both a 3×4 and 4×4 keypad. Your table will probably look different5.

keypad_lut:
    db "D#0*C987B654A321"

And the code for the actual lookup loads the base address of the table into the HL register pair and the index into BC. ld l,(hl) loads the key character into L.

;Lookup the key value in the table
    ld e,a              ;Pop into E for outputting
    
    ;Lookup character in lookup table
    ld hl,keypad_lut
    ld c,a
    ld b,0
    add hl,bc
    ld l,(hl)           ;Key in L, saved for later

And we can output the character to the LCD:

    ld c,lcd_data    ;LCD command port
    out (c),l           ;Position LCD cursor

    jr keyboard_scan    ;Loop infinitely
Finished keypad scanning and conversion to an ASCII character. On the left is the index into the lookup table (hex). On the right is the character read from the lookup table.

Adding a Delay

You’ll notice that every time we press a key we get a lot of flicker on the LCD. This is because our Z80 is running so fast that it reads the keypad multiple times for every key press. For now we’ll fix that with a delay loop such as that below. This ties up the processor so it can’t do other things. Long term there are be better ways to do this but they need extra hardware which we’ll get to later.

    ld bc,$0000         ;Delay loop - prevent multiple keypresses
sleep_loop:
    dec c
    jr nz,sleep_loop
    dec b
    jr nz,sleep_loop

Full source code and KiCad files can be found in the Github repo.

Final Thoughts

You’ll also notice if you read the full code that I’ve put a wait loop before every write to the LCD. This makes the code horribly painful to read6. We can clean this up by putting the LCD code into a subroutine. But we can’t do that without any RAM. So next time we’ll be adding that to the system, at which point it should finally feel like we have an actual, usable, computer on our hands.

And before I go, here is my RC2014 keyboard attached to the breadboard computer. The code for this is also in the repository.

Reading keys by scanning the RC2014 keyboard.

Buying Links

Footnotes

  1. The same way as a bus line can be tri-stated when it is not outputting and effectively disconnected from the circuit.
  2. My spell checker thinks this should be ‘matrices’ but matrixes feels more correct to me so I’m using that until I find out otherwise.
  3. Remember that, in active low logic, OR functions as an AND – the ouput will only be low in both inputs are low
  4. It is also good practice to either tie up or down any unused inputs to logic chips.
  5. And there’s no reason you have to look up characters. If you were building, say, a calculator you could look up the numeric value for each key or tokens to represent the functions of other keys.
  6. I’ve also put those loops before evaluating values to be displayed. This keeps the code simpler but calculating the values while the display might be busy and testing for busy immediately before displaying a value would be more optimal.

5 Replies to “Couch To 64k Part 4: Adding a Keypad/Keyboard to our Z80 Breadboard Computer”

    1. Next time I’ll be adding RAM which will mean I can start creating actual software to run on it. Some kind of monitor is high on the list. I’ll probably start by writing something basic and then look at converting/patching something more fully functional to run on it.

  1. Very interesting to read. Like you I’ve been following Ben Eater’s excellent how to build an 8-bit computer. I was also heavily into Z80 programming when I was a lot younger. Started with ZX80 then moved onto ZX81 and then ZX Spectrum.
    Can’t wait to see your next instalment.

    1. That’s pretty similar to my history. ZX80, ZX81, ZX Spectrum and the onto the Amstrad CPC464. I wish I could have kept the Spectrum though. It was a very early one and probably rather collectable today.

Leave a Reply

Your email address will not be published. Required fields are marked *