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:

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.

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...

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.