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.
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
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.
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.
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
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.
Inside the keypad is a matrix of wires as shown in the diagram.
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.
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.
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.
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:
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.
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
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)
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
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
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
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.
- Crystal oscillator: Ebay (Aff) Amazon (Aff) 4MHz in a DIP-14 package. You can use other values provided they don’t exceed your Z80s maximum speed rating but if so you may need to tweak some of the software.
- Matrix keypad: Ebay (Aff) Amazon (Aff) Choose one which is convenient for connecting to a breadboard.
- RC2014 universal micro keyboard, Tynemouth Minstrel keyboard, Tynemouth PET commodore keyboard, The 8-bit Guys Petskey keyboard.
- 74LS273: Ebay (Aff) Amazon (Aff) (or 74HCT273).
- 74LS244: Ebay (Aff) Amazon (Aff) (or 74HCT244).
- 74LS32: Ebay (Aff) Amazon (Aff) (or 74HCT32).
- 1N4148 diodes: Ebay (Aff) Amazon (Aff)
- 10k resistors: Ebay (Aff) Amazon (Aff)
- 0.01uF ceramic capacitors: Ebay (Aff) Amazon (Aff)
- The same way as a bus line can be tri-stated when it is not outputting and effectively disconnected from the circuit.
- 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.
- Remember that, in active low logic, OR functions as an AND – the ouput will only be low in both inputs are low
- It is also good practice to either tie up or down any unused inputs to logic chips.
- 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.
- 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.