A z80-based Embedded Design – Hot Tub Heater

A

The 8-bit Z80 microprocessor has been a popular choice for early computer designs and continues to see some use in embedded systems today. When I was 14 years old, my Dad purchased a Commodore 128 as an upgrade to our C64, which contained both a 6510 and a Z80 microprocessor. In the 128, it was intended to be used as a platform to run CP/M, an OS intended for business computing, which the C128 never really excelled at.

The Z80 is a simple microprocessor designed in the mid 1970s with a rich set of instructions and plenty of useful registers. In a quest to meet a self-imposed goal of designing the most “pure” discreet component-based embedded system, I chose this processor since I already had experience with 6502 designs and associated assembly code. I have designed systems using microcontrollers like the ATmega328 used in the Arduino (see photos below), but they generally “hide” the underlying architecture such as address and data lines while offering higher-level functionality like general purpose I/O, A/D converters, timers, counters, oscillators, USARTS, etc. Using a microprocessor requires that the engineer design those functions externally into the system, and have everything work properly in the end.

One of the features of the Z80 that I find very useful is the IORQ pin, which allows your peripherals to use the same address space as your ROM or RAM without interfering with those circuits.

I created a design and drew up an initial schematic using KiCad. In my design, I have the capability of 8 different I/O devices, using an additional chip, the 74AHC138, as glue logic. In the initial design, I wanted to map the Hitachi LCD module to I/O address 0x00. This is done by setting bits A7, A6, and A5 to zero. The 74AHC138 has multiple select lines for enabling the 1-of-8 decoder, which has two active-low enable pins to detect the Z80’s IORQ going low and one active-high enable pin to detect when M1 is concurrently high. To accomplish this, I tied the Z80’s M1 pin to the 74AHC138’s A3 and the IORQ pin to both E1 and E2. When both conditions are met the decoder will enable one of its 8 outputs based on the three digital address inputs, A2, A1, and A0. By mapping the Z80’s A7, A6, and A5 address lines to these inputs on the chip, we end up with 8 different I/O combinations. For instance, if you were to supply 0, 1, 0 for the A7, A6, and A5 address lines of the Z80, the chip would output 11111011 to its 8 output lines. The conversion is shown in the chip’s data sheet:

74AHC138 Decoding Table

In this configuration, the Z80’s address lines A7, A6, and A5 are used to select the appropriate I/O peripheral, while the remaining lines, A4, A3, A2, A1, and A0 can be used for setting registers on the peripheral, if necessary. It’s always a tradeoff between the number of peripherals and the number of available registers, so if more than 5 registers are needed, a more complex design or fewer peripheral devices are necessary.

To test this configuration, I had to teach myself the basics of the Z80 instruction set and registers. It’s quite a different processor than the MOS Technologies 6502 and 6510 chips. There a more registers and instructions, making assembly coding a little more tedious. In any case, here’s the listing that displays “Hello World!” on the LCD:

; ====================
;
; I/O Testing w/LCD
;
; ====================

; ROM Size
RomSize:    equ 8000h   ; For 32kB Rom size.

; RAM
RamTop:     equ 0xffff   ; Top address of RAM

 ; Program start address
  org 0x0000

main:       ld sp, RamTop ; Stack pointer set to the top of RAM
            call timer_11ms ; 11 ms delay from power on for the LCD reset circuit
            call init_disp
            ld hl, Hello_TXT
            call print_str

loop:
            jr loop

lcd_busy:
            in a, (0x02) ; check the busy flag bit using the RW flag
            and %10000000
            jr nz, lcd_busy
            ret

print_str:
            call lcd_busy
            ld a, 0xff
            ld (0x8000), a
            ld a, (hl)
            and a
            ret z
            out (0x01), a
            inc hl
            jr print_str

init_disp:
            call lcd_busy
            ld a, %00111000 ; 8-bit mode; 2-line display; 5x8 font
            out (0x00), a
            call lcd_busy
            ld a, %00001111 ; Display on; cursor on; blink on
            out (0x00), a
            call lcd_busy
            ld a, %00000110 ; Increment and shift cursor; don't shift display
            out (0x00), a
            call lcd_busy
            ld a, $00000001 ; Clear display
            out (0x00), a
            call lcd_busy
            ret

timer_11ms:
            ld e, 0xff
j60:
            ld b, 0x08
j61:
            dec b
            jp nz, j61
            dec e
            jp nz, j60
            ret

Hello_TXT:
            db "Hello world!",0

End:
; Padding with 255 to make the file of 32K size (can be 4K, 8K, 16k, etc) but

    ds 0000h+RomSize-End,255    ; 8000h+RomSize-End if org 8000h

Getting the timing correct for the LCD took some debugging. I failed to read the LCD documentation fully at first and struggled to understand why it wasn’t responding properly. Turns out that there is a power-on reset circuit that holds the chip disabled for 10 milliseconds after power on. Adding the appropriate delay in the assembly code took some work, since the instructions each use a different number of clock cycles to complete. I’m running this design at 8 MHz, and using the Saleae Logic Pro 16 was a huge help in figuring out the timing.

I will be adding a UART for serial communications next…to be continued.

About the author

Chip Barrere

Chip is an electrical engineer, software developer, and meteorologist.