The clock is controlled by an ATTiny87 which has three main jobs:
- Counting the pulses from the Maxim DS32kHz
- Controlling the display via the shift registers (read more)
- Interacting with the user via the reed switches to produce a user interface
Each of these jobs will be discussed separately below as well as the main code to bring it all together in a power efficient way. Full code can be found at my GitHub.
1. Keeping time
The DS32kHz is a simple (but expensive) temperature compensated oscillator from Maxim Integrated that generates a 32.768kHz square wave with an accuracy of 7.5ppm, or 4 minutes per year.
The ATTiny needs to count these pulses. Every 32768 pulses, it can increment the time by one second. For this we use the 8-bit hardware timer/counter called Timer0. This timer can operate asynchronously so the processor can go to sleep and the timer will keep counting. We initialise the counter in a function called initialiseTimeCounter which is called once when the clock is powered on.
First, we set the time to a starting time of 21:41:00 (24 hour clock). Because the timer is operating asynchronously, we need to be very careful that we follow the procedures in the datasheet to prevent register corruption.
The next two lines enable asynchronous operation from the pulses on the XTAL1 pin (where the DS32kHz is connected). The timer is then put into Compare Match mode (CTC) which means that each time the counter counts up to a certain value (specified in register OCR0A which we set to 31 in the next line) the timer resets. Finally, we want an interrupt to trigger each time the timer resets, so we enable this. The interrupt is called TIMER0_COMPA. The function waitForAsynchronous ensures that enough time has passed for the data to successfully be written to the asynchronous registers of the timer so no corruption occurs.
Why does the timer only count to 31 instead of 32768? We are using a /1024 prescaler that causes the clock rate to be divided by 1024. The clock frequency (32.768kHz) divided by 1024 is 32 and therefore the timer should reset every time it reaches 31.
Enabling the prescaler causes the timer to begin. We therefore set the prescaler in the function startTimeCounter.
So now we have the timer resetting and triggering an interrupt once per second and we just need to define what happens each time. We define this in a special function called ISR(TIMER0_COMPA_vect). All we want to do is increment the time by one second. For this we use a function called incrementSeconds that really is as simple as it sounds. Every 60th time, incrementSeconds will call incrementMinutes which will in turn call incrementHours every 60 times.
Finally, we need need to able to stop the clock, which is done in stopTimeCounter.
This is very similar to startTimeCounter, but it sets the prescaler to stop the clock.
2. Controlling the Display
The display is controlled via four 8-bit shift registers in series (making one large 32-bit shift register). Each bit in the shift register corresponds to one digit in the display, so by setting the correct bits we illuminate the correct digits. Originally, this was to be controlled using the hardware SPI bus, but given certain issues, we are bit banging the data.
The function initialiseShift is called at power-on. This function sets the latch, reset, data and clock pins for the shift registers as outputs. Then we take the shift registers out of reset and prepare the latch pin in a high state (as it should be when idle). Now that we have the pins configured, we are ready to send data.
Our most fundamental function is called shiftByte which generates pulses on the clock pin and shifts one bit at a time onto the data line. The data line is changed on each falling edge and shifted onto the shift registers on each rising edge. The clock period is approximately 2ms. This could decreased if desired.
However, we also need to toggle the latch line in order for the data to be visible on the outputs of the shift registers (and thus change the displays). We want to do this after we send four bytes (one for each shift register), so we use a function called shift which takes an array of four bytes. This calls shiftByte four times and generates a rising edge on the latch line.
In reality, we don’t want to shift arbitrary bytes to the shift registers – we want to show the time. Therefore, our highest level function is called displayTime.
Here we generate two 16 bit integers called data_h and data_l representing the top and bottom 16 bits of the 32-bit shift register. Both are defaulted to zero value, so if the time is invalid nothing will be displayed. The four digits to be displayed are calculated using divide and mod functions. Digit1 is the tens of hours, digit2 is the unit hours, digit3 is the 10s of minutes and digit4 is the unit seconds.
In order to find which bits in the two 16-bit integers should be set in order to illuminate the correct number on the Nixie tube, we have a look-up table for each Nixie tube called nixie1, nixie2 nixie3 and nixie4. Note that, nixie1 will only every display the values 0, 1 and 2 and therefore the look-up table need only be of length three.
Using this we shift the bits into the correct position, break it into four bytes and use the shift function to send this data to the shift registers.
Finally, we need a way to clear the display. We could use the shift function to send all zeros to the shift registers, however it is quicker to use the reset line.
To do this, clearDisplay causes a rising edge on the reset line, followed by a rising edge on the latch line, therefore causing all zeros to be latched on the shift registers and clearing the display.
3. User Interface
The clock has three modes TIME_MODE, HOUR_SET_MODE and MINUTE_SET_MODE. These modes need to be navigated by the user by using switches. The clock has two switches which are implemented using reed switches so they are closed by holding magnets to the wood of the clock.
In the this mode, the clock simply shows the time. The clock is quite bright and can be disturbing at night when trying to sleep. Therefore, when the right-hand switch (RHS) is closed the display goes blank as long as it is held down. When in TIME_MODE mode, the ATTiny follows this path in its main loop:
Firstly, if the left-hand switch (LHS) is closed, we want to enter HOUR_SET_MODE which allows us to set the hour. Otherwise, if the RHS is open then we display the time using our displayTime function and if RHS is closed we clear the display.
After waiting enough clock cycles to ensure that the asynchronous Timer0 is not corrupted, the ATTiny then enters sleep mode. In this case this means it enters power-save mode. This is the second lowest power mode after power-down mode.
Every second, the ATTiny is woken from sleep mode by the TIMER0_COMPA interrupt caused by Timer0. This would not be possible in power-down mode as Timer0 is not enabled in this mode.
Once the ATTiny is awake, it continues the loop from where it left off and repeats the same code again, eventually going back to sleep. While awake, the green LED is illuminated producing a very short pulse on the LED as the ATTiny is only awake for a tiny fraction of each second. This can be used as a heartbeat to check the ATTiny is running but isn’t very visible once the circuit board is in its box.
HOUR_SET_MODE and MINUTE_SET_MODE
When the device enters HOUR_SET_MODE from TIME_MODE, there are a few tasks required to handle the transition.
As we enter HOUR_SET_MODE, we stop Timer0. This prevents the time ticking while we set the time. We then start the user interface clock via the function enterUI.
The user interface clock is simply another hardware timer/counter in the device, called Timer1. The set up is similar to Timer0 except that it is clocked from the 1MHz system clock via a 1024 prescaler and resets at 244, therefore triggering the TIMER1_COMPA interrupt approximately four times per second. Because Timer0 is no longer running and the system clock is stopped during sleep, the ATTiny cannot go to sleep in this mode as the timers would never wake it up as no interrupts would be triggered.
Therefore, in the main loop if we are not in TIME_MODE we simply display the time over and over again. Using the flags blanking_minutes and blanking_hours, we are also able to selectively blank either the minutes or the hours on the display.
The user interface clock is able to take advantage of these flags in order to produce a blinking display so that when setting the hours, the hours flash on the display and when setting the minutes, the minutes flash on the display.
This is achieved in the TIMER1_COMPA interrupt.
Depending on whether we are in HOUR_SET_MODE or MINUTE_SET_MODE we toggle the blanking flags appropriately. Furthermore, every second cycle (every half second) we poll the switches. If the RHS is closed, we appropriately increment the hours or the minutes. If the LHS is closed we progress to the next state, either MINUTE_STATE_MODE or TIME_MODE.
This allows us to progress from TIME_MODE to HOUR_SET_MODE using the LHS, then set the hours using the RHS, then progress to MINUTE_SET_MODE using the LHS, then set the minutes using the RHS and finally return to TIME_MODE using the LHS.
On entering TIME_MODE once again, we stop the user interface clock and restart Timer0 so the clock keeps time once again.
The code is well segregated into one timer that simply counts the time, one timer that simply controls the user interface, and the main loop that renders the result of the timers to the display.
It would be ideal if the ATTiny could enter sleep mode while setting the clock, waking only four times a second to handle the UI. However, this would require using the same timer/counter (Timer0) to handle both the UI and the time-keeping as only Timer0 can operate asynchronously. While obviously possible, it would result in worse segregation of code for minimal benefits in power reduction.
Overall, this system allows the clock to use minimal power by putting the ATTiny into power saving mode for the vast majority of the time, only waking once per second to update the display and also during the setting of the clock. Power saving could be improved by not flashing the green LED while the ATTiny is awake (although this uses very little power) and also by turning off the 180V power supply while the display is blanked, however this would require hardware changes.