Couch to 64k Part 3: Adding a Character LCD Display to our Z80 Breadboard Computer

In the previous part of this series we attached a ROM chip to our Z80 so we can run programs. But running programs is of little use unless our computer has some way to communicate with the outside world. We need input and output (I/O). In this part we'll discuss how input and output work on the Z80. We'll then add a character LCD display to our breadboard computer so it can display useful information.

LCD display with a bootup screen

Parts Needed

For this session we'll be adding:

  • Two extra breadboards.
  • A 74LS138 3-to-8 line decoder.
  • A 74LS00 or 74HCT00 quad NAND gate (see text)
  • A 10k ohm variable resistor (or a selection of resistors - see text below).

As always, see the end of the article for affiliate buying links.

Input and Output on the Z80

Recall from the previous part we used the ld (hl),a instruction to write to memory. This put the contents of the HL register pair on the Z80's address bus, the contents of the A register on the data bus and, as we saw in part 1, asserted the /MREQ (memory request) and /WR signals. If we had any RAM attached these signals would have told it to store the given data at the specified address.

We also touched on output with the instruction out (42),a. This put the value 42 on the address bus, the contents of the A register on the data bus, and asserted the signals /IORQ (I/O request) and /WR.

The value, 42, here is referred to as an I/O port address which, at least to me, makes it sound very different to a memory address. But, as far as the Z80 is concerned, it's the same1 as writing to a memory address, but using the /IORQ signal instead of /MREQ. The real difference is how this is handled by the rest of the computer.

For memory accesses we have a large number of memory locations in a small number of memory chips. However, with I/O we're using port addresses to distinguish between different devices. Thus, we may have devices to handle displays, keyboards, serial communications, parallel communications etc. Our hardware port addresses are used to specify the device we are writing to or reading from. Some of these devices will have number of registers which can be written to and/or read from. For example there may be separate registers for writing commands and data to. Port addresses are also used to denote which of these registers we wish to access.

Address Decoding

The above sounds a little academical. So let's get practical and start building our hardware to connect to the LCD.

Side note: During input and output the Z80 puts data on all 16 of it's data pins. However, different I/O instructions use the high byte of the address in different ways. I don't want to go into exactly what happens at the moment2 so, for this article I'm going to assume that port addresses are 8 bits wide. Thus we have port addresses between 0 and 255 ($00 to $FF) and using address lines A0 to A7.

Chararacter LCD pinout
Chararacter LCD pinout

Remember from our discussion of memory chips in part 2 that pretty much any device we want to connect will have some kind of device enable input which we can use to signal that the Z80 wants to talk to that device. The LCD display is no different and, looking at the pinout above, we have the E(nable) pin for that purpose.

Since this is the first I/O device we want to attach to our computer then we may as well assign it the port address 0. So, we need a circuit which asserts the E pin of the display when the Z80 writes to hardware port 0. In Z80 hardware design it's common to use a 74LS1383 chip to do this.

74LS138 pinout
74LS138 pinout

The 74LS138 takes a three bit address (values 0 to 7) on pins A0 to A2 (sometimes labelled A, B and C) and asserts one (and only one) of the pins /Y0 to /Y7 based on that address. I.e. we have the truth table below:

A2 A1 A0 Asserted pin
0  0  0   /Y0
0  0  1   /Y1
0  1  0   /Y2
0  1  1   /Y3
1  0  0   /Y4
1  0  1   /Y5
1  1  0   /Y6
1  1  1   /Y7

It also has three enable pins, /E1, /E2 and E3 (sometimes labelled G1, /G2A and G2B). An output pin will only be asserted if all three of the enable pins are asserted. If one or more of the enable pins aren't asserted then no output pin will be asserted.

We can put the some of the Z80s address lines onto the address lines of the '138 and use the Z80s control signals to control the enable lines. When the correct control signals are asserted one of the output enable signals will be asserted.

You may be worrying that we can only decode three bits of the Z80s address lines. In other words, if we connect Z80 pins A0 to A2 to the '138s pins A0 to A2 then we have no way of knowing that the Z80s A3 to A7 (or even A15) pins are all zeros.

And the answer to that is simply that it doesn't matter. It's perfectly acceptable (and very common) to have multiple port addresses mapping to the same hardware port. But as long as we can uniquely identify each hardware device then it doesn't matter. As long as we can write to our devices using the assigned port addresses we don't need to worry what the unassigned port addresses are doing.

Decoder Wiring

So we connect the Z80's A0, A1, A2 to the '138's A0, A1, A2 and we're done? Not so fast kiddo. While the following is a matter of personal preference it's also the way everyone else does it, so it's probably a good method to stick to.

Remember above that I said that our device may have multiple registers, such as to select between command and data, and that port addresses are used to determine which register is selected? In other words we'll use one or more address lines to select the desired register. Common practice is to use the least significant address lines for this. So, it we have a single line to select between command and data registers we'll use the A0 address line. If there's four registers to select between we'll use A0 and A1.

This design also makes things friendlier on the software side. Using port $80 for a command and port $81 for data is easier to work with than using port $08 for command and port $88 for data.

So we use the most significant address lines for the device address and the least significant for register address.

I think we're finally at the point were we can start wiring this thing up. We'll connect Z80 address lines A7, A6 and A5 to the '138's A2, A1 and A0 pins respectively.

And, because we only want our device to be enabled when we're doing an I/O operation we'll connect the Z80's /IORQ signal to one of the '138's active low enable inputs.

We also need to pay attention to the /M1 signal here. When the Z80 reads from or writes to an I/O device it takes /IORQ low along with either /RD or /WR. But it can also take /IORQ low along with /M1. This is the interrupt acknowledge signal used as part of the interrupt handler (which we'll come to in a future article). For now you just need to know that the /IORQ signal will be accompanied by either /RD, /WR or /M1. If we're also decoding (testing for) the /RD or /WR signals we can safely ignore /M1. But in this case we're only decoding /RD (see below) with an assumption that, if the Z80 isn't reading then it must be writing (when /IORQ is low). So, in this case we need to verify that /M1 is high. Fortunately the '138 has an active high enable pin which can do that.

Our '138 has an enable input left. We could hard wire it but a better solution is to connect it to an address line. This means we have some room for future expansion. If we ever need more than eight devices we can add another '138, and invert the same address for it's equivalent enable signal. I'm going to use address line A4 here so we're using the four high bits of the address for the device address with the lower four bits giving ample space for the device itself.

Schematic for the 74LS138 address decoder
Schematic for the 74LS138 address decoder

At this point I need to raise the issue of input versus output versus input and output. For todays project we'll be both writing to and reading from the LCD display but there are other devices which are purely output or purely input. If we (or whoever writes the softare for our system) accidentally reads from a write-only device there probably won't be any ill effects, but trying to write to a read only device will could well result in the device outputting data to the bus at the same time as the processor, which could create a short circuit and permanantly damage one or other device.

It's good design to also use the Z80s /WR and /RD signals when addressing such devices. You can do this my using separate address decoders, or using either /WR or /RD as an input to the address decoder (either enable or and address input depending on your needs) or by ORing4 the /WR or /RD signal against the device enable signal being output by the '138. We'll see this demonstrated in the next part in this series.

Connecting the LCD Display.

Now that we've connected the Z80 to the '138 be need to connect both to the LCD display.

Pinout for a typical character LCD display
Pinout for a typical character LCD display

In the world of devices with parallel interfaces there are two main connection schemes used, 6800 and 8080. The 6800 scheme is that used by the 6800 processor (and also the 6502 which was, in many ways, a copy of it) and the 8080 scheme as used by the 8080 and thence by the Z80 (which, of course, copied and extended it).

There are two important differences between the two schemes. Firstly the 8080 scheme uses active low enable inputs whereas the 6800 scheme uses active high enable inputs. Secondly the 8080 uses separate /WR and /RD inputs whilst the 6800 scheme uses a single combined R/W input pin which is high when reading and low when writing5.

The active high enable pin is easy to control: we can just invert the active low signal from our '1386.

The R/W pin appears more challenging. I actually have an entire article describing the solution. It turns out that all we need to do is invert the Z80s /RD signal. (The short explanation of how this works is that the inverted /RD signal means the LCDs R/W will be high when reading and low when writing. If we directly connected the /WR signal the /WR signal would have disappeared before the LCD samples the data and address (register select) lines. This is because the LCD samples the inputs after the enable signal ceases, and the enable signal stops being asserted (because /IORQ stops being asserted) at the same time the /WR signal stops being asserted).

Finishing Up

Now we just have to connect to the RS (register select) of the LCD to the A0 of the Z80 and to connect the data pins of both devices together.

Other connections are as per the schematic. I didn't have a breadboardable variable resistor handy so I used some ordinary resisters instead. You'll need the resistance between V0 and the 5V line to be about 10k and that between the V0 and ground to be between 100 and 1k ohms. Remember that resistors in series give a combined resistance which is the sum of their resistances. Resistors combined in parallel have a total resistance which is the average of their values.

Whether you're using resistors or a potentiometer, adjust the resistance so you can just see the letter grid on the display.

LCD with contrast adjusted
LCD with contrast adjusted

There's one final comment I want to make about the schematic. I'm using NAND gates instead of inverters for the enable and R/W signals because I feel the spare NANDs may have some further use in later stages of the design.

Full schematic with LCD connected to our Z80 computer
Full schematic with LCD connected to our Z80 computer

Wiring this up on the breadboard should be straightforward. Just remember that the Z80's data pins are not in sequence and the LCD's data pins run, roughly, in the opposite direction. Also, we'll be redoing the data lines in the next part so don't be too fussy with them.

Z80 pinout diagram
Z80 pinout
Breadboard computer showing LCD wiring
Breadboard computer showing LCD wiring
Breadboard computer showing LCD wiring
Breadboard computer showing LCD wiring

Software

And with that done we get to write some software.

It's time to refer to the datasheet for the display. Character LCDs usually use either the HD4480 or ST7066U driver ICs and, fortunately, both of these chips are mostly compatible. If we take a look at the ST7066U datasheet page 8 (Pin Function) we see that we send commands and read status information with the RS pin low. And we send and read datawith the RS pin high.

Recall that we have connected Z80 address line A0 to R/S and we've connected the LCDs enable pin via Q0 of the '138. Thus we have our LCDs command register on port $00 and the data register on port $01. So we can start sending characters with,

out ($01),'A'

Well, not quite yet. We have to send some commands to configure the display before we can start using it. The setup sequence is on page 23 of the datasheet.

With reference to the command listing on page 17 we get the following startup code:

;Pt3.1LCD.z80

;Constants
lcd_command equ $00 ;LCD command I/O port
lcd_data equ $01    ;LCD data I/O port
    
org 0
    ld a,$3f        ;Function set: 8-bit interface, 2-line, small font
    out (lcd_command),a
    ld a,$0f        ;Display on, cursor on
    out (lcd_command),a     ;(I find turning the cursor on is very helpful when debugging)
    ld a,$01        ;Clear display
    out (lcd_command),a
    ld a,$06        ;Entry mode: left to right, no shift
    out (lcd_command),a

We can then add some code to display a startup message:

    ld hl,message   ;Message address
message_loop:       ;Loop back here for next character
    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

message:
    db ">HELLO< >DOCTOR< >NAME< >CONTINUE< >YESTERDAY< >TOMORROW<",0

Assemble and run this and you may find the output is garbled. Why? Because the LCD takes a few moments to process each command and it's possible for our Z80 to send data too fast for it. To be fair, at the moment we're clocking the Z80 via the Arduino which gives the LCD plenty of time to process everything. But, later we'll be adding a crystal to clock the Z80 at much higher speeds. When we do we'll almost certainly be sending the data too fast.

Wait a Moment Kiddo

Fortunately the LCD has a method to test if it is busy or not, as shown on page 17 of the datasheet. We need to read back from the command register (port $00) and, if busy, bit 7 of the returned data will be set. We can do this with the code below:

lcd_wait_loop:         ;Loop back here if LCD is busy
    in a,($00)         ;Read the status into A
    rlca               ;Rotate A left, bit 7 moves into the carry flag
    jr c,lcd_wait_loop ;Loop back if the carry flag is set

Those last couple of instructions need a but of explanation. There's a number of ways we could test if bit 7 of A is set. We could use the bit test instruction,

    bit 7,a            ;Set the carry flag equal to bit 7 of A
    jr c,lcd_wait_loop ;Jump if carry set

or we could AND the A register with $80 - binary %10000000,

    and $80
    jr nz,lcd_wait_loop

or, as I've done, using the RLCA instruction - rotate left through carry - which moves all the bits if A left one place, puts what was in bit 7 into the carry flag and also back into bit 0 of A.

It looks confusing and non-obvious so why did I do it that way? Because both of the other solutions I gave require a two byte opcode. The BIT instruction is in an extended command set which needs a prefix byte, the AND instruction takes the $80 parameter as a second byte, whereas RLCA is a single byte command. Thus it takes up less memory and is quicker to execute.

Hacks like this are part of the reason that assembler code is normally heavily commented. I want to show you techniques like this so that you'll be better prepared for reading other peoples assembler code, and also because, if you progress beyond being a hobbyist, such techniques are useful to use in your own code. Of course, if you're new to assembler then have a browse of the instruction set to explore some stuff and see what works.

But, returning to our wait loop. As I said, we probably don't need to use it yet, but bear it in mind if exploring on your, own and we'll add it in later when we need it.

Go Explore

If you want to explore on your own I'd suggest trying to send the setup commands in a loop, like we did with the data. If so, what terminator byte is best to use? (You don't want to use anything which might be a command). Or you could set the length into a variable and create something akin to a FOR loop. If so, take a look at the DJNZ instruction.

And why not take a deeper look at the LCDs command set? It has some interesting feature such setting the cursor position, scrolling text, reading characters back and even 'user defined characters' (design your own characters).

Until next time, have fun.

Schematics, files and other resources can be found on my Github repo.

Buy Links

Footnotes

  1. There are some timing differences but these don't affect us.
  2. It's complicated.
  3. I'm using 74LS chip names in this series. You can also substitute 74HCT series chips unless told otherwise. LS series chips are the ones in use when the Z80 was popular but these days the HCT chips are easier to get hold of and, also, generally cheaper.
  4. Remember we're talking inverted logic here, so an OR is effectively an AND gate.
  5. And since the device is only active when enabled we can safely ignore what happens to the R/W (or indeed /RW and /WR pins) at other times
  6. We could use the 74LS137 which is the same as the '138 but with active high outputs but we have more devices to connect which will want active low enable signals.

15 Replies to “Couch to 64k Part 3: Adding a Character LCD Display to our Z80 Breadboard Computer”

    1. I’ve rewritten that paragraph to make it clearer. Not exactly a ‘problem’ but /IORQ is also used as part of the interrupt acknowledge signal – when /M1 also goes low. If you’re testing for /RD and/or /WR as appropriate then you can ignore /M1. In this case we’re only testing /RD and assuming there’s a write happening in /IORQ is low and /RD is high, so we need to handle the case where /IORQ and /M! are low and both /RD and /WR are still high.

  1. Thank you very, very much for your excellent guide and instructions – I’m slowly building my Z80 breadboard computer along with your posts, and I had a real ‘holy wow’ moment just now when I got my first ‘HELLO WORLD!’ out on my LCD. I’m also loving the trivia and insight you bring to the instructions – I feel like I’m learning a lot, even though so much of it is still going over my head.

    1. It’s a real thrill when it works isn’t it. If you want to learn deeper I’d recommend the Ben Eater 8-bit computer videos on YouTube. He explains the basics of digital electronics really well, and that’s where I learnt most of what I know.

      1. I love the Ben Eater videos, you’re being my co-guides! I’m using his 555 timers for the computer clock. I’ve got a question, actually – on your videos, the text wraps to the next line of the LCD display. When I run your code, the text doesn’t wrap. Is that something I need to code, or is it a feature of the ST7066U I can enable?

        1. It sounds like you’ve solved it, but once you’ve got it into two-line mode you can reposition the cursor, as you’ve done, or the text autowraps to the next line once the buffer for the first line is full (and back to the first line after the second line etc). IIRC the buffer is about 64 characters per line and you can use the command set to scroll the text left and right without having to update the text.

          If you have a four line display the second half of the buffer for lines one and two become the buffer for lines three and four, which is confusing but retains backwards compatibility.

      2. Delighted to follow this up – not only did I work out that I was sending the wrong command and never switching the display to two-line mode, but I modified your assembler code with a DJNZ instruction to switch the display to the next line after 16 characters. Feeling very accomplished!

  2. Hey Mike,

    I thought this might interest you. I took your breadboard design and made it into a circuitboard. I swapped the Arduino for a configuration centered around the 555 IC. It’s a simplified version of Ben Eater’s clock circuit. I’m anticipating some future debugging that would require stopping the clock and single-stepping through each executed command.

    Hope you like it.

    https://www.dropbox.com/s/y3emxbn9y8fvc24/CouchTo64K.png?dl=0

      1. I hear you. I’ll also like to make this design more compact before making the Gerbers available. I’m wondering if you have the intention of going for anything larger that a 16×2 display, though.

        1. Sorry, I’ve only just noticed your reply.
          Yes, character LCD displays are horribly limiting. I’d like to upgrade to some form of colour LCD panel. The challenge is finding one which runs at five volts and has a parallel interface for easy connection. Failing that I’ll go for a three volt one with SPI which shouldn’t be too hard to hook up. I’d also like something with an onboard character generator to save having to send dozens of pixels for each character cell.
          Mike

  3. Hey, Mike.
    I’m eager to try this out but first the parts need to arrive. However, could you tell me where the code which detects if the screen is goes. Is that at the beginning or at the end of the first part of code?

    1. You need to detect if the display is busy before every write to it. Since the wait loop code uses the A register you’ll need to add it before you assign to the A register the value to be output. Since you now have multiple copies of the wait code you’ll also need to give the loop label a unique name in each (and don’t forget to change the name in the jump instruction!) When we get to adding RAM then we can put this stuff in a subroutine and it will all get much much easier 🙂

      If you scoot forward to Part 4 https://bread80.com/2020/10/15/couch-to-64k-part-4-adding-a-keypad-keyboard-to-our-z80-breadboard-computer/ I begin by installing a crystal and give the modified code with the busy wait loops to make it work.

      PS. If you’re still using the slow clock then the Z80 is running so slowly that you don’t need to check if the LCD is busy.

Comments are closed.