24-hour analog clock

De Wiki LOGre
< Horloge analogique 24h
Révision de 8 septembre 2017 à 11:50 par Ebonet (discuter | contributions) (Battery life : new section)

(diff) ← Version précédente | Voir la version courante (diff) | Version suivante → (diff)
Aller à : navigation, rechercher
Language: Français  • English


Project by: Edgar Bonet.

This is DIY, analog, 24-hour, one handed wall clock. The sole hand points downwards at midnight and upwards at noon, so that its movements mimics the path of the Sun in the sky. We can optionally stick a piece of paper on the clock’s face, with drawings evocative of day and night activities.

Clock24 finished.jpg


Electronics and mechanics

Clock movement, driving circuit and power supply.

This is based on a standard quartz clock movement, modified in order to be driven by a microcontroller. The microcontroller is mounted on a stripboard circuit and powered at 3 V by a pair of AA cells. The motor is driven by current pulses of alternating polarity. This is very clearly explained on this blog post, which was my main inspiration for this project:

Controlling a clock with an Arduino, by mahto

In order to have a 24-hour single hand clock, we only have to:

  • remove the hours and seconds hands, and keep only the minutes hand;
  • slow it down by a factor 24, in order to complete a revolution in 24 h instead of one hour.

In a regular clock, pulses are sent to the motor every second. We will then pulse the motor every 24 seconds instead.

Modifying the clockwork

The clockwork case is easy to open with a screwdriver. Once open, all the parts (gears and PCB) can be removed with no tool. They can also messily fall on the table if one is not cautious. The PCB, which comes with the motor coil, has to be removed. Then:

  • unsolder and keep the quartz, it will be useful later;
  • cut at least one of the traces connecting the integrated circuit (hidden under the blue resist in the pictures) to the coil;
  • solder two wires to the PCB pads that are connected to the coil;
  • make a hole on the case through which the wires can be pulled out.

Here are a few pictures taken while putting it back together:

Electronics

I initially went for an Arduino-based clock, just like the project by mahto cited above. A first try with an Arduino Uno showed that this was not feasible. First reason is the power drain of the Arduino which, at around 40 to 50 mA, is incompatible with battery operation. The second reason is the Arduino time base being a ceramic resonator, far too inaccurate for a clock.

I then went for a bare microcontroller. I choose an Atmel AVR ATmega48A because:

  • it is almost identical to the ATmega328P powering the Arduino Uno, and I am learning about microcontrollers on an Uno;
  • it has all the necessary hardware to build a clock.

The only noticeable difference between the 48A and the 328P is the amount of memory: 4 KiB of flash and 0.5 KiB of RAM on the 48A, vs. 32 and 2 KiB respectively on the 328P. But the 48A has more than enough resources for this project, and it costs next to nothing.

The essential reference:

Here is the circuit diagram:

Clock24 circuit.png
Circuit built on stripboard.

The motor is driven by pins PD0 and PD1. The LED on PC5 is for checking the MCU (short for “microcontroller”) is working: it blinks three times at power-up, then goes off in order to save battery. The connector on the right is for programming the MCU.

Compared with mahto’s schematic, this one has two simplifications:

  • there is no resistor in series with the coil because the coil itself has 204 Ω internal resistance, which is enough for 3 V operation;
  • no flyback diodes because I forgot to include them the MCU already has them on each of its outputs (c.f. fig. 14-1, page 75 of the datasheet).

Here are the pictures of both faces of the circuit. Pin 1 of the MCU would be on the top of both pictures. In order to save space, three jumpers (GND, VCC/AVCC and RESET) run below the DIP holder. Only two are visible on the pictures. The RESET jumper runs between the pins of the DIP holder.


Programming the microcontroller

The AVR is used in its default fuse configuration, which means it will run at 1 MHz from the internal 8 MHz oscillator prescaled by a factor 8.

Using the internal oscillator makes pins 9 and 10 available for connecting the quartz crystal to the asynchronous counter. Those are the TOSC1 and TOSC2 functions of these pins. Furthermore, at this frequency the MCU works over its whole range of supply voltages, from 1.8 to 5.5 V (fig. 29-1, p. 303 of the datasheet). The lower 1.8 V limits means it can be power from a pair of 1.5 V cells until the cells are practically exhausted.

How it works

The AVR has a peripheral named “8-bit Timer/Counter2 with PWM and Asynchronous Operation” documented on section 18, p. 141. This will do most of the work. It includes:

  • an “oscillator”, which is actually an amplifier for keeping the quartz oscillating;
  • a prescaler for dividing the quartz frequency by 1, 8, 32, 64, 128, 256 or 1024, at our choosing;
  • an 8-bit counter;
  • some logic for generating interrupts on counter overflows;
  • the required I/O registers for configuring the whole thing.

C.f. fig. 18-2, p. 142.

The AVR has several sleep modes (c.f. section 10, p. 39) for reducing power consumption. The one we will use is POWER_SAVE. This mode shuts down practically everything in the MCU except for the Timer/Counter2, which is the wake-up source. In this mode, the supply current is around 2 µA.

The nominal frequency of the quartz is 32768 Hz. Setting the prescaler to 1024, we get a counter overflow, and hence an interrupt, every 8 seconds. We can set the prescaler to 128 if we want one interrupt per second instead.

Having the counter run asynchronously relative to the system clock requires some special care: after accessing any of its configuration registers, and before putting the MCU to sleep, we must check the ASSR (Asynchronous Status Register) and wait for the *UB (“Update Busy”) flag related to the previously accessed register to be clear. Inside an interrupt handler, we must do a dummy write on one of the configuration registers and, once again, wait for the *UB flag to be clear. C.f. section 18.9, p. 151.

In order to compensate for the discrepancy between the real and nominal quartz frequencies, we perform a calibration, then use the following algorithm, similar to Bresenham's algorithm for drawing slanted lines:

for (each interrupt) {
    unaccounted_microseconds += MICROSECONDS_PER_INTERRUPT;
    if (unaccounted_microseconds >= MICROSECONDS_PER_PULSE) {
        send_pulse_to_motor();
        unaccounted_microseconds -= MICROSECONDS_PER_PULSE;
    }
}

where MICROSECONDS_PER_PULSE is 24×10⁶ (for sending a pulse every 24 seconds) and MICROSECONDS_PER_INTERRUPT, nominally 8×10⁶, is tuned to the measured quartz frequency. This ensures that the average pulse frequency is correct.

Battery life

Since this clock is in service, it has been powered by the same pair of no-name AA cells, which are now more than 5 years old. Here is the time-evolution of their voltage:

date battery level
June 2012 new (voltage not measured, nominally 2 × 1.5 V = 3 V)
October 2014 2.85 V
September 2017 2.61 V

From this table giving the voltage of an alkaline cell as a function of its remaining capacity, it would seems these cells are near the middle of their useful life. This shows the POWER_SAVE mode is really effective for preserving battery life.

Program

/*
 * AVR clock: Drive a watch crystal and a clock movement.
 *
 * Connect:
 *   - power (-) to GND (pins 8, 22)
 *   - power (+) to VCC and AVCC (pins 7, 20)
 *   - watch XTAL to TOSC1/TOSC2 (pins 9, 10)
 *   - clock motor to PD0/PD1 (pins 2, 3)
 *   - LED + resistor to PC5 (pin 28) and GND
 *
 * Alternatively, #define PPS and get a 1PPS signal from PD0 (pin 2).
 *
 * TC2 prescaler is set to 1024, then TC2 will overflow once every
 * (roughly) 8 seconds and trigger an interrupt.
 *
 * If PPS is #defined, then the TC2 prescaler is set to 128 and TC2 will
 * overflow once per second.
 */
 
/* Output 1PPS on pin 2. */
/* #define PPS */
 
#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#define F_CPU 1000000UL  /* 1 MHz */
#include <util/delay.h>
 
#ifdef PPS
#   define MICROSECONDS_PER_INTERRUPT  1000000
#   define MICROSECONDS_PER_PULSE      1000000
#else
/* Nominally 8e6, but quartz was calibrated to be 6.5 ppm fast. */
#   define MICROSECONDS_PER_INTERRUPT  7999948
#   define MICROSECONDS_PER_PULSE     24000000
#endif
 
/*
 * On 3 V supply, seems to work reliably for pulse lengths between
 * 52 and 76 ms, or >= 100 ms
 */
#define PULSE_LENGTH 70  /* ms */
 
/* Interrupt triggered once per second. */
ISR(TIMER2_OVF_vect)
{
    static long unaccounted_microseconds;
    static uint8_t pin_bit = _BV(PD0);
#ifdef PPS
    static const uint8_t pin_mask = 0;
#else
    static const uint8_t pin_mask = _BV(PD0) | _BV(PD1);
#endif
 
    /* Send pulses at the correct average frequency. */
    unaccounted_microseconds += MICROSECONDS_PER_INTERRUPT;
    if (unaccounted_microseconds < MICROSECONDS_PER_PULSE) return;
    unaccounted_microseconds -= MICROSECONDS_PER_PULSE;
 
    /* Set the OCR2AUB flag in ASSR. */
    OCR2A = 0;
 
    /* Pulse motor. */
    PORTD = pin_bit;
    _delay_ms(PULSE_LENGTH);
    PORTD = 0;
    pin_bit ^= pin_mask;  /* next time use the other pin  */
 
    /* Wait end of TOSC1 cycle (30.5 us). */
    while (ASSR & _BV(OCR2AUB)) {/* wait */}
}
 
int main(void)
{
    uint8_t count;
 
    /* Configure PC5, PD0 and PD1 as output. */
    DDRC = _BV(DDC5);
    DDRD = _BV(DDD0) | _BV(DDD1);
 
    /* Blink while waiting for the crystal to stabilize. */
    for (count = 0; count < 3; count++) {
        _delay_ms(167);
        PORTC = _BV(PC5);
        _delay_ms(167);
        PORTC = 0;
    }
 
    /* Save power by sleeping. */
    set_sleep_mode(SLEEP_MODE_PWR_SAVE);
 
    /* Configure Timer/Counter 2. */
    ASSR |= _BV(AS2);               /* asynchronous */
    TCCR2A = 0;                     /* normal counting mode */
#ifdef PPS
    TCCR2B = _BV(CS22) | _BV(CS20); /* prescaler = 128 */
#else
    TCCR2B = _BV(CS22) | _BV(CS21) | _BV(CS20); /* prescaler = 1024 */
#endif
    TCNT2 = 1;                      /* initial value */
    while (ASSR & _BV(TCN2UB)) {/* wait */}
    TIMSK2 = _BV(TOIE2);        /* Timer Overflow Interrupt Enable */
    sei();
 
    for (;;) sleep_mode();
}

Compiling and loading

I compiled this on Linux with avr-gcc. For uploading into the AVR, I am using my Uno as an ISP programmer. I have read contradictory information on the Web about this method. I should say it works for me provided I:

  • modify the standard arduino_isp program for a slower transfer speed of 9600 b/s (in setup() : Serial.begin(9600));
  • give this same speed as a command-line argument to avrdude;
  • put a 1 µF capacitor between +5V and RESET on the Arduino.

With the following Makefile:

TARGET=clock.hex
CFLAGS=-mmcu=atmega48 -Os -Wall -Wextra
AVRDUDE=avrdude -p m48 -c avrisp -b 9600 -P /dev/ttyACM0
 
all:	$(TARGET)
 
upload:	$(TARGET)
	$(AVRDUDE) -e -U flash:w:$(TARGET)
 
test:
	$(AVRDUDE) -n  -v
 
%.elf:	%.c
	avr-gcc $(CFLAGS) $< -o $@
 
%.hex:	%.elf
	avr-objcopy -j .text -j .data -O ihex $< $@

I just type:

  • make: for compiling;
  • make test: for testing the communication with the AVR;
  • make upload: for transferring the program into the AVR.


Quartz calibration

Given the limited resolution of the clock display, calibrating its quartz is an exercise in futility. But it is fun, which is a valid enough reason for doing it. ;-) Calibration is performed by compiling the program in PPS mode (with #define PPS, PPS meaning “pulse per second”), and comparing its PPS signal with an NTP server.

Link to the PC via Arduino

I am using the Arduino for relaying the PPS signal from the AVR to my PC. The AVR and the Arduino are connected as per the following schematic:

Clock24 arduino link.png

Here, the diode and the associated 1 kΩ resistor feed the AVR with a supply voltage close to 2.7 V, which simulates the supply of half-drained cells. The transistor and the resistor at its base shift the signal from 2.7 to 5 V. These four parts (diode, transistor and resistors) are connected on a breadboard.

It should be noted that the internal pullup on pin 2 of the Arduino has to be activated. The level shifter inverts the signal: the rising edges of the PPS are detected as falling edges by the Arduino. These edges trigger an interrupt that sends a single character (a dot) on the serial port. Here is the code:

/*
 * Forward a 1PPS signal from a digital input to the serial port.
 *
 * Intended for calibrating a clock.
 *
 * Connect the inverted 1PPS signal to digital pin 2.
 */
 
#include <avr/sleep.h>
 
#define PPS_PIN 2
#define PPS_IRQ 0
 
void forward()
{
  Serial.write('.');
}
 
void setup()
{
  Serial.begin(9600);
  pinMode(PPS_PIN, INPUT);
  digitalWrite(PPS_PIN, HIGH);  // enable pull-up
  attachInterrupt(PPS_IRQ, forward, FALLING);
}
 
void loop()
{
  sleep_mode();
}

Data acquisition on Linux

The PC, running Linux, reads the PPS signal and compares it with an NTP server using the two programs below:

  • time-pps.c monitors the serial port with a select() loop. Each time a character becomes available, it reads the current time with gettimeofday() and stores the time difference in a file.
  • time-ntp.sh, called every 10 minutes by cron, queries an NTP server and records in another file the difference between the NTP time and the PC time.

The PC clock is left free-running, with no local NTP server. We then get two files with the time differences (PPS − PC_time) and (NTP_time − PC_time). Taking the difference between these two files gives (PPS − NTP_time), which is the interesting data.

Results

Drift.
Allan variance.

Here is the data:

  • in red: (PPS − PC_time)
  • in green: (NTP_time − PC_time)
  • in blue: (PPS − NTP_time), computed as the difference of the previous two.

In this graph, the average drift has been estimated by linear regression and subtracted from the data. This is in order for the drift fluctuations to be visible. On the (PPS − NTP_time) series, the average drift was +6.5 ppm, i.e. the clock would be roughly 3.4 minutes fast over one year. This drift rate is generally acceptable, but now that we know it, we will fix it. We compute the time elapsed between interrupts, and we define

#define MICROSECONDS_PER_INTERRUPT 7999948

instead of the nominal 8000000.

The second graph is the (square root of the) Allan variance. These are the frequency fluctuations of the quartz as a function of the time scale over which we measure them. The strong fluctuations at short time scales are the jitter of the measuring technique. On longer time scales, frequency fluctuations fall below 10⁻⁷, which is comforting.


Clock face

Laser engraved and cut clock face.

The face is 350 mm in diameter. It has a 7.5 mm central hole that fits the threads around the hand axes. It has been engraved and cut at the Fab Lab de La Casemate on MDF, using a laser cutter, from this SVG file:

This file was generated by a PHP script looping over the hours. Here is the source code of this script:

The face diameter can be customized by changing the global scale factor (scale(2.0669)) on line 13 of the SVG, or line 26 of the PHP file. The diameter of the central hole (the circle line 16 on the SVG, line 29 on the PHP) has to be adjusted accordingly to still fit the threads.