Z80 Text Scroller

For some time now, I've had a bunch of Z80 processors on hand. To be exact, Z0840004PSC (they don't state "Z80 CPU" on them), which are the 4MHz NMOS version (dynamic logic, and big stinking current consumption). So I decided I'd learn Z80 assembly, get an assembler (Telemark Assembler by Squak Valley Software seems to work well enough) and put together an NVRAM reader/writer to write the machine code for the Z80. That done, all that remains is to breadboard the Z80 and support components. Fortunately, the Z80 is very easy to use (far easier than that goofy 8086!), having bus signals, non-multiplexed data and address bus all available for straightforward use. Here's the circuit:

Schematic

The reset pin really should have a larger time constant (ten miliseconds, say) and a schmitt trigger. As is, I think it's coming out of reset before the clock is stable; with the CMOS Z80, this is probably not a problem, but on the NMOS, it does nothing until manually reset. Oh well, I didn't bring the parts necessary to really make it work.

Breadboarded

This is the circuit, as built, "installed" next to my room door. Data lines are mostly red, address (and LED connections) mostly blue. Signal quality on the breadboard is just barely good enough; under some conditions I was getting erratic display (though the program appeared to run correctly). It seems to be fine at the moment. The ribbon cable goes to the front of my door, where the eight digits of LEDs are installed.

Now, as shown, the circuit doesn't do a single thing. As with most microcontroller projects, it's all in the code. The assembly is shown below:


; Z80 testing!
; By Tim Williams, 02-2009.

REFRESHES_PER_LOOP	.equ 30		; number of refresh cycles per loop

;
; -=-=- Code -=-=-
;

.cseg

.org 0

;
; Some initialization
;

		ld ix,stack
		ld sp,ix		; set up stack pointer
		ld a,REFRESHES_PER_LOOP
		ld (framesloop),a	; initialize framesloop
		ld a,endmsg-msg
		ld (msgcount),a		; and count

; Preconvert ASCII-coded message to seven-segment bit patterns

		ld hl,msg		; source pointer
		ld de,msgconv		; destination pointer
		ld b,a
convloop:
		ld a,(hl)
		call segconvert
		ld (de),a
		inc hl
		inc de
		djnz convloop		; repeat until done

;
; Main loop
;

		ld hl,msgconv
		ld (msgpos),hl		; save starting message offset
main:
		ld a,11111110b
		ld (digit),a
		ld hl,(msgpos)		; get message pointer
		ld b,8			; number of passes
digitloop:
		ld a,11111111b
		out (1),a		; turn off display
		ld a,(hl)
		out (0),a		; change segments
		ld a,(digit)
		out (1),a		; turn on digit
		rlca
		ld (digit),a		; rotate to the next digit
		inc hl
		call delay

		djnz digitloop		; B = B-1 and repeat until 0

		ld a,(framesloop)
		dec a			; done all the frames yet?
		ld (framesloop),a
		jr nz,main		; no, get back to work

; Reset frame counter, advance to next position in message and check if at end

		ld a,REFRESHES_PER_LOOP
		ld (framesloop),a	; reset frame counter

		ld hl,(msgpos)
		inc hl			; yup, go to the next position
		ld (msgpos),hl

		ld a,(msgcount)
		dec a
		ld (msgcount),a
		jr nz,main

; reset count at end of message

		ld a,endmsg-msg
		ld (msgcount),a
		ld hl,msgconv
		ld (msgpos),hl		; set starting message offset
		jr main

;
; A fixed delay loop, 256 passes.
;
delay:
		ld a,0
delayloop:	dec a
		jr nz,delayloop
		ret

;
; Converts an ASCII code in A to a seven-segment code in A.
; To enable the character's dot, set bit 7.
;
segconvert:
		bit 7,a			; no extended characters
		jr nz,outconv
		sub 30h			; check if A is in range
		jp m,outconv
		cp 5ah-30h+1
		jp p,outconv

		push hl			; save pointer register
		ld hl,sevenseg
		add a,l			; ok, now find index into the table
		jr nc,nocarry
		inc h			; carry into H
nocarry:	ld l,a
		ld a,(hl)		; get it and, we're done
		pop hl
		ret
outconv:
		ld a,0			; unprintable characters are zeroed
		ret

;
; -=-=- Data -=-=-
;

.dseg

; ASCII message to scroll around
msg		.byte "       HELLO WORLD"
endmsg		.byte "       "

; ASCII-to-7-segment converted message
msgconv		.block endmsg-msg+7

;
; Variables
;

digit		.byte 0
msgcount	.byte 0
msgpos		.word 0
framesloop	.byte 0

;
; Seven segment display ASCII conversion table.
; Some characters really suck and should be avoided:
;   K, M, V, W and X are the worst.
;   "q" looks like "9", "Z" looks like "2", "S" looks like "5"
; Bits are: (dot)(g)(f)(e)(d)(c)(b)(a) (seven segment plus dot).
;
sevenseg
; starting at ASCII offset 30h: hexadecimal numerals
.byte 00111111b, 00000110b, 01011011b, 01001111b, 01100110b	; 0, 1, 2, 3, 4
.byte 01101101b, 01111101b, 00000111b, 01111111b, 01100111b	; 5, 6, 7, 8, 9
.byte 01110111b, 01111100b, 01011000b, 01011110b, 01111001b	; A, b, c, d, E
.byte 01110001b						; F
; 40h: alphabet
.byte 0							; "@" unprintable
.byte 01110111b, 01111100b, 01011000b, 01011110b, 01111001b	; A, b, c, d, E
.byte 01110001b, 01101111b, 01110100b, 00000110b, 00011110b	; F, g, h, I, J
.byte 01110000b, 00111000b, 00110111b, 01010100b, 01011100b	; K, L, M, n, o
.byte 01110011b, 01100111b, 01010000b, 01101101b, 01111000b	; P, q, r, S, t
.byte 00111110b, 00011100b, 01111110b, 01110110b, 01101110b	; U, v, W, X, y
.byte 01011011b						; Z
; >5Ah: unprintable, unused

		.block 10h
stack

.end

Because this code accepts ASCII, it begins by converting the string to a seven-segment coded string, using a conversion table. Inside the loop, a fixed delay (since I don't have a timer to trigger periodic interrupts) lights each digit in sequence, producing one refresh frame. Each character takes about a milisecond (the "count to 256" loop takes about 1μs for the decrement and 3μs for the conditional jump), so one refresh takes about 8ms and 30 refreshes (one frame) takes 240ms, about a quarter of a second. In this way, text scrolls left at 4 characters/second, a reasonable rate.

How's it look? Well, on the front side of the door it looks something like...

Display

At this moment, part of the message was "HOME OF TIM WILLIAMS"... obviously, the "M" and "W" don't display well on only seven segments, and unfortunately my name has three such unprintable characters...

...But what good is a static photograph anyway? Here's video of the first model, using only two segments. Not easy to read words, but it works nonetheless! I still had two latches in here, which means expanding to four, then eight, digits was a few more jumper wires and a change of about three constants in the program! Not a bad deal.


Return to Electronics


Web page maintained by Tim Williams. All rights reserved.