Timer Project

The objective of the project was to create a timer module that could trigger a relay multiple times at a predetermined interval.  The timer would be used for interval-timer photography, such as recording a 3D print to play back in super fast motion. Although this project uses the timer to operate a relay that is connected to a camera shutter release, it could instead trigger a LED to signal an optical device, or trigger a shutter directly.  Currently the relay is external to the unit, but there is space in the case for including an internal relay.

PARTS.

U1 Arduino Pro Mini. I chose the Pro over the Nano because I do not expect to be re-programming it and the USB interface could not be justified. The programming pins are accessible if needed.
L1 128x64 OLED display with I2C interface to display the settings and the progress.  Header pins and sockets to suit.  This model OLED uses a different colour for the top few rows, and the code takes advantage of this.
SW1 Momentary SPDT switch for up/down control. Salvaged from an old CD drive.  Two single pole momentary switches could also be used, but the 'rocker' style of this switch is very easy to use.
SW2 Momentary SPST switch for reset. Salvaged from somewhere.
SW3 SPST On/Off switch. Salvaged from the PCB of some old piece of gear.
D1 LED and matched R1 resistor (eg 220ohm).
Hookup wire and prototype board (see below).
Q1 Small NPN power transistor. (Eg 2N2222) and resistor as required for the load to be driven (eg 1k)
B1 9v Battery with clip and leads.
3D printed case.

Timer PartsThe switches can be anything suitable, but the height needs to be considered if they are all mounted at the same level on the prototype board.  The 'rocker' style switch used for the up/down adjustment has five pins but only three connections - each pair of outer pins is the same side of the switch. 

The OLED display is a common, easily available item.   For this project I mounted it using the header pins inserted into a matching header socket mounted to the breadboard. These displays are also available with an ISP interface - either would work for this project, but the extra speed of ISP is not required.  The display sits below the cutout in the lid and is supported by the header pins and a small packing piece under the substrate. I avoided a more solid mount because of the risk of fracturing the delicate glass face plate.

The prototype board I used is marked 2-110 and is similar to this. It is actually designed for the Pro module to straddle the centre row, but as I am only using a few pins for the module it was easier to put it at the bottom of the board and leave space for the other components at the top.

THE CIRCUIT

Timer SchematicThe 9v battery power is provided via the on/off switch to VRaw of the Pro.  Vcc from the Pro is wired to the OLED.  The grounds of both devices are tied to battery ground.  The up/down switch for setting the timer period is connected to data pins 2 and 3 (active low) and the start/stop button is connected to data pin 5 (active low).    Output to the transistor and the relay is at data pin 8   There is no particular reason for choosing these pins - any data pins would work as well.  The OLED is driven from the stadard UNO/Pro SDA and SCL pins.

ASSEMBLY

Timer LayoutThe Pro has header pins soldered on two edges and for the programming pins, but not across the fourth side as this would make things too crowded on the prototype board.  I soldered leads to the prototype boards before soldering the Pro on, as with this particular board the traces to the header pins are only available between the header rows. Although this makes things a bit tricky it saves a lot of board space and is much easier than trying to solder on the trace side of the board.  I also soldered some leads to some of the unused pin traces in case I wanted to add new features later on.  The hookup wire came from an old parallel printer cable.  This wire is easily tinned and soldered, the covering is very heat resistant, it is quite flexible, and the multitude of colours means that the wire can indicate the function of the connection, reducing the risk of mistakes. 

THE CASE

Timer CaseThe case is a simple two-part assembly, with a lid that sits down into the base without any attachment. The lid would be a good candidate for a magnetic attachment.  The base is divided into two parts - one is a tight fit for the prototyping board and the other is a large area for the battery. I did not divide this area because I might want to install the relay, which currently swings freely at the end of a wire, inside the case at some time in the future. Foam spacers can be used to stop the battery sliding around.  The relay lead exits the case through a hole near the bottom left corner of the prototype board.

I printed knobs for the reset and power switches so they would match the case, and added a small surround for the LED to make it look a bit neater. I filled the mounting holes for the OLED with plugs, after deciding that I did not want to screw the device down to the lid.


COMPLETED

Timer CompleteThe completed unit, with the lid pushed down into the base.  When the unit is is turned on the interval between ticks can be set using the momentary rocker switch - right to increase and left to decrease.  Timing is started and stopped with the large momentary switch to the left of the display. If this is pressed when the timer is running then the display is inverted to show that timing is paused.   The current interval and the number of ticks are displayed.  If start/stop is pressed again the count is cleared to 0 and timing starts.

CODE

The timer uses a timer library with interrupts. There are several timer libraries available for the Arduino, so substituting a different one would not be a problem. The interrupt-handling routine breaks most of the rules for using interrupts, but as the time interval is completely controlled by the code it doesn't create a problem. The timing interval can be set from one second to 9999 seconds.  To make this possible with a simple user interface acceleration is used - each time the interval is increased or decreased the rate of change is raised by a factor of 1.2.  So holding the switch in one direction or the other gradually increases the rate at which the interval increases or decreases.  The full range of 1 to 9999 takes about 15s.  If the switch goes back to neutral the step is reset to 1, so that at any point the interval can be set to an accuracy of 1s by briefly flicking the switch.  

Code is included to save the currently selected interval to EEProm so that it can be retrieved at startup and set as the default.

/**************************************************************************
  Timer using OLED based on SSD1306 drivers

  This sketch uses a 128x64 pixel display using I2C to communicate.
  4 pins are required to interface.

  Vcc --> +5v
  Gnd --> Gnd
  SCL --> A5
  SDA --> A4

  2-colour OLED has 16 lines of yellow and 48 lines of blue (pixel
  lines are numbered 0 to 63).
  Oled library is at: https://adafruit.github.io/Adafruit_SSD1306/html/class_adafruit___s_s_d1306.html

  Timer library is at: http://www.doctormonk.com/2012/01/arduino-timer-library.html

 **************************************************************************/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Timer.h>
#include <EEPROM.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Timer t; // Create the timer instance.
long period;
int8_t tickEvent;
long count = 0;
String pMessage;
String cMessage;

const int buttonUpPin = 2;     // the number of the button pin for UP
const int buttonDnPin = 3;     // the number of the button pin for DOWN
const int buttonRsPin = 5;     // the number of the button pin for RESET
const int clickPin = 8;        // then number of the button pin for CLICK

int buttonUpState = 0;         // variable for reading the button UP status
int buttonDnState = 0;         // variable for reading the button DOWN status
int buttonRsState = 0;         // variable for reading the button RESET status
float buttonStep = 1.0;        // Step per button press

boolean running = false;       // the state of the timer

void setup() {
  // Serial.begin(9600);         // Needed if debugging the display
 
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
    // Serial.println(F("SSD1306 allocation failed"));  // Uncomment if the display doesn't start.
    for (;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  // display.display();               // Don't need it.
  // delay(500); // Pause for 2 seconds

  // initialize the button and clicker pins:
  pinMode(buttonUpPin, INPUT_PULLUP);
  pinMode(buttonDnPin, INPUT_PULLUP);
  pinMode(buttonRsPin, INPUT_PULLUP);
  pinMode(clickPin, OUTPUT);
  digitalWrite(clickPin, LOW); // Initialize the click output.

  // Get the saved period
  period = readLongFromEEPROM(0);
  if (period < 1 || period > 9999L) period = 5;

  // initialize the timer and show the first screen
  tickEvent = t.every(period * 1000, doClick, NULL);
  DrawScreen();
}

void loop() {
  // read the state of the buttons
  buttonUpState = digitalRead(buttonUpPin);
  buttonDnState = digitalRead(buttonDnPin);
  //  If neither button is pressed, reset the button acceleration
  if (!(buttonUpState == LOW || buttonDnState == LOW)) buttonStep = 1.0;

  // check if the UP button is pressed. If it is, the button state is LOW:
  if (buttonUpState == LOW) {
    // Bump the Period
    period = period + long(buttonStep);
    // Bump the acceleration
    buttonStep *= 1.2;
    if (period > 9999L) {
      period = 9999L;
    }
    // Restart the timer
    t.stop(tickEvent);
    tickEvent = t.every(period * 1000, doClick, NULL);
    // Debounce
    delay(250);
    DrawScreen();
  }

  // check if the DOWN button is pressed. If it is, the button state is LOW:
  if (buttonDnState == LOW) {
    // Debump the Period
    period = period - long(buttonStep);
    // Bump the acceleration
    buttonStep *= 1.2;
    if (period < 1L) {
      period = 1L;
    }
    t.stop(tickEvent);
    tickEvent = t.every(period * 1000, doClick, NULL);
    // Debounce
    delay(250);
    DrawScreen();    //
  }

  buttonRsState = digitalRead(buttonRsPin);
  // check if the RESET button is pressed. If it is, the button state is LOW:
  if (buttonRsState == LOW) {
    // Change the timer state.
    display.invertDisplay(running);
    running = !running;
    if (running) {
      count = 0;      // Iniitialize the click counter
      writeLongIntoEEPROM(0, period); // Update the saved period.
    }
    // Serial.println("Reset");
    // Debounce
    delay(250);
    DrawScreen();
  }

  if (running) {
    t.update();
  }

}

void DrawScreen(void) {
  display.clearDisplay();

  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(7, 0);
  display.println(F("PhotoTimer"));

  display.setTextSize(2);
  display.setCursor(3, 21);
  display.println(F("Delay:"));
  display.setCursor(75, 21);
  display.println(int(period));
  display.setCursor(3, 46);
  display.println(F("Count:"));
  display.setCursor(75, 46);
  display.println(int(count));
  display.display();
}

void doClick(void *context) {
  count += 1;
  digitalWrite(clickPin, HIGH);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(500);
  digitalWrite(clickPin, LOW);
  digitalWrite(LED_BUILTIN, LOW);
  DrawScreen();
}

long readLongFromEEPROM(int address)
{
  long number = ((long)EEPROM.read(address) << 24) +
         ((long)EEPROM.read(address + 1) << 16) +
         ((long)EEPROM.read(address + 2) << 8) +
         (long)EEPROM.read(address + 3);
         return number;
}

void writeLongIntoEEPROM(int address, long number)
{
  EEPROM.write(address, (number >> 24) & 0xFF);
  EEPROM.write(address + 1, (number >> 16) & 0xFF);
  EEPROM.write(address + 2, (number >> 8) & 0xFF);
  EEPROM.write(address + 3, number & 0xFF);
}