Having built both a digital clock and a Z80 microcomputer, the logical next step was to combine the two ideas and build a digital clock powered by a Z80 microcomputer. Using a microcomputer has the advantage of allowing for more complex functionality without complicating the circuitry, because much of the work can be done in software. Unlike my first digital clock, this one automatically sets the time to 12 AM when it is powered up, and the whole display blinks (indicating that power has been disconnected) until the time has been set. The set buttons are also easier to work with. Pressing either the hour or minute buttons briefly will advance the time, but if either button is held down, the time will rapidly advance, so that the buttons do not have to be repeatedly pressed and released. Any of these features could have been added to my hard-wired design, but at the expense of adding more chips.
The brain of the clock is the Z80 processor. It executes the control program, which is stored in a 64 kB EEPROM chip. The system has no RAM. The control program is simple enough that all the necessary variables can be stored in the Z80's registers. The disadvantage of having no RAM is that I cannot use any subroutines (since there is nowhere to store a call stack), but this just requires copying and pasting code in places where I would otherwise use a subroutine. If I absolutely needed a subroutine, it would be possible to store the return address in a register, and jump to that address at the end of the subroutine instead of using a RET instruction (which pops the return address off the top of the call stack). However, the code is simple enough that it did not seem worth the trouble.
The control program is entirely interrupt-driven. The interrupts come from an 8253 Programmable Interval Timer. The 8253 contains 3 16-bit counters, which can be programmed to operate in several different modes. Counter 0 is programmed as a frequency generator (mode 2). In this mode, after a value has been loaded into the counter, the counter repeatedly counts down from that value, generating a pulse (active low) each time the counter reaches 0. I load the value 8, which results in a 125 kHz (1 mHz/8) signal from the output of counter 0. This signal is used as the input for both counters 1 and 2.
Counter 1 is also programmed to operate in mode 2, with a count of 62500. This divides the 125 kHz input clock by 62500, resulting in a 2 Hz output frequency. This signal is used to interrupt the CPU twice every second.
It is also possible for the CPU to receive an interrupt from the set button. Both the set button and the timer output are active low, so combining them with an AND gate (actually constructed from an OR gate and 3 inverters to avoid adding an extra chip) causes the CPU to receive an interrupt either when counter 1 generates a pulse, or the set button is pressed. The CPU determines the cause of the interrupt by reading the status of three buttons and checking if the set button is down. In either case, the CPU ends the interrupt by halting (as opposed to a RETI instruction), because the RETI instruction is useless without a call stack, and the CPU is always halted when it is not servicing an interrupt.
If the interrupt is triggered by the set button, the CPU first checks the status of the other two buttons. If neither is pressed, the CPU enables interrupts (interrupts are disabled at the start of the interrupt handler), and halts. The next interrupt will happen immediately after interrupts are enabled if the set button is pressed, so as long as the set button is down the CPU will continuously poll the buttons.
If the hour or minute button is pressed, the CPU needs to update the time. When the CPU detects that one of these buttons is down, it enters a busy wait which limits the rate at which the time is updated. This wait cannot be interrupted, because interrupts are disable while the interrupt handler is running. The first time the CPU enters the wait loop after a button is pressed, the delay is slightly longer. Thus after a button is pressed, there will be a short pause and then the time will rapidly advance. This system also debounces the buttons, so that I do not need external debouncing circuitry.
If an interrupt comes from the timer (i.e. the set button is not pressed), the CPU first decrements a counter stored in one of its registers. Since the interrupts come two times per second, the counter starts with a value of 120, so that it takes exactly one minute to reach zero. When the count reaches zero, it is reloaded with a value of 120 and the time advances. I use 2 interrupts per second to control the blinking. The colon (or the entire display if the time has not been set) blinks once per second with a 50% duty cycle. Thus I need two interrupts per second to control it, one interrupt to blink on and one interrupt to blink off.
The time is stored in 3 registers. Two of the registers store the hours and minutes (respectively) in BCD form. A single bit in the third register determines if the time is AM or PM.
Advancing the time requires both updating the registers that store the time and updating the display. The displays are controlled by an EDE707 Display Controller. The EDE707 stores the values for the display in its own memory, so I only need to update the values for the digits that change. An update is done by first sending the EDE707 a command which tells it which digit is to be updated, followed by a value for the digit. While the EDE707 reduces the complexity of the circuitry needed to power the displays, making the CPU communicate with it was tricky. The EDE707 needs any command to remain on its inputs for at least 1.2 ms. This corresponds to 1200 ticks of the CPU clock. Normally, an OUT instruction (which sends data to an I/O device) only leaves the data on the bus for a couple of cycles. This problem could be resolved by adding an external buffer, but to avoid adding another chip I devised a system to create the necessary delay using the third timer in the 8253.
To force the data to stay on the bus longer, I needed to use the Z80's WAIT signal. The WAIT signal comes from an OR gate that combines the output of the 8253's third counter (inverted to make the signal active high), and the WRITE signal for the EDE707. Normally, the inverted output from the timer is low and the WRITE signal for EDE707 is high, and so the WAIT signal is high (inactive). When the OUT instruction decodes to the port address of the EDE707, the WRITE signal goes low, and the WAIT signal immediately goes low as well, causing the Z80 to enter the wait state. This will cause the data to remain on the bus until the WAIT signal goes high again.
Counter 2 in the 8253 has the job of disabling the WAIT signal after 1.2 ms. It is programmed in mode 5, hardware-triggered strobe. In mode 5, when the gate signal goes high, the counter will count down from its initial count (set by the CPU) until it reaches 0, and then lower the output for one period of the input clock (which in this case is the output of counter 0). The input clock frequency is 125 kHz, so one tick is 8 microseconds, or 8 CPU cycles. With an initial count of 150, the delay will by 150 x 8 microseconds = 1.2 ms. Thus when the gate signal goes high, the output of counter 2 will go low for 8 CPU cycles after a 1.2 ms delay.
The signal that controls the WAIT line and the WRITE signal for the display controller is inverted to create the gate signal for counter 2, so that when an OUT instructions goes to the display controller, the WAIT signal for the CPU goes low, the WRITE signal for the display controller goes low, and the gate signal for counter 2 goes high. Counter 2 then starts counting down from 150 until at reaches zero, after exactly 1.2 ms. At this point, its output goes low for one period of the input clock (or 8 periods of the CPU clock). This signal is the inverted and then fed to the other input of the OR gate that controls the WAIT signal, so that when counter 2 reaches 0 and the output goes low, the WAIT signal will go high. The output of counter 2 will remain low for 8 CPU cycles. This is plenty of time for the I/O operation to complete, so that when the output of counter 2 goes high again, the WRITE signal for the display controller is high, causing the WAIT signal to remain high until the next time I send data to the counter. The one potential pitfall of this design is that if I do one write to the counter immediately after another, the output of counter 2 may still be active, causing the WAIT signal to remain inactive. To remedy this problem, if I want to do several writes to the display controller (for example sending a digit number followed by a value), I need to insert a few NOP instructions (or other instructions if there is something useful to do) between the writes to make sure the counter has time to reset.
In addition to the digits, the EDE707 controls the colon between the hours and minutes and the PM LED. All three LED's (two for the colon and the single PM LED) are controlled by the first digit. Since the colon blinks and the PM LED may be on or off at any time, all four states (AM colon on, AM colon off, PM colon on, PM colon off) must be realizable by sending the appropriate numbers to the EDE707. By carefully choosing which segments connect to which LED's, I was able to get all four combinations using a single digit. The picture below shows how this is achieved:
The image below shows the board removed from the case, with all the major components labeled:
The 3 debugging LED's labeled in the picture are normally off. There's a switch that enables them for when I'm debugging the control program. One simply indicates that the debug switch is on, one monitors the HALT signal, and one monitors the RESET signal. Adding LED's was easier than constantly testing these signals with my logic probe.
Here's a shot of the underside. The buttons on the bottom are redundant set buttons, so that I can set the time when the clock is not in its case (for debugging). There is also a reset button (also useful for debugging).
The LED power MOSFET (also labeled) was not included in the schematic for the EDE707 provided in the datasheet. I added it to cut power to the LED's when the CPU's RESET signal is active. A resistor and a capacitor are used to activate the CPU's RESET signal briefly when the clock powers up. While the RESET signal is active, the CPU is powerless to send any commands to the EDE707, which means that the display will read 00.00 briefly after power-up until the CPU commands it to display 12:00. This minor problem was easily fixed by using the MOSFET to cut power to the LED's while the RESET signal is low. As soon as the RESET signal is disabled, the CPU commands the EDE707 to display the correct initial value of 12:00.
Here is a YouTube video I made to demostrate the clock in action: