Camera Intervalometer Source Code

Canon Camera Controller – nickname C-Cubed

img_3902

img_7595

The system contains the Arduino Mega micro processor, a 20 x 4 LCD display, a TSL230R light to frequency convertor, the DT1307 real time clock and a SHT10 temperature and humidity sensor. Please note that the SHT10 sensor is not fully compliant with the I2C protocol, it will be replaced by the SHT21 sensor which is compliant.

The following arduino software code has been developed to control most digital cameras. It allows you to define the interval between photographs and the the length of the pulse to trigger the camera. If the camera is in Auto or Manual modes then the pulse length should be around 50-60ms; in Bulb mode the pulse length can be increased to accurately control the bulb exposure time or used for bulb ramping in a time lapse sequence.

The code has been developed to avoid the use of delay() functions, except in the setup loop, this means that the buttons and LCD display remain fully functional and exposure times can be changed between photographs.

Mechanical and electrical details of the project can be seen here


ccc_main.pde is the main code, the remaining blocks should be put into individual tabs inside the Arduino interface.

History
v1.5 – first release
v1.6 – added routine that saves Interval and Exposure values to EEPROM and reloads them next time the system is used. Press Red button on startup to load default values.
v1.7 – minor bug fixes relating to the LCD display, zip file updated.

The Arduino code can be downloaded from here as a zip file. Updated to v1.7 13/06/10

Please be aware that the code blocks shown below may not be the latest version but the zip file will always be the latest version.


ccc_main.pde

/*
 Canon Camera Controller (nickname C-Cubed)
 version 1.6 - 03/06/10
 Author      - K Lewis www.photosbykev.com

 The system uses the normal camera pulses to trigger the camera in auto/manual modes.
 The pulse length can be adjusted so that it can also control the bulb exposures.
 All timing loops are non blocking so the buttons and lcd remain functional.

 Tested on Canon 7D and 40D cameras but will work on most systems with minor changes

 History
 v1.5 - first release
 v1.6 - added routine that saves Interval and Exposure values and reloads them next time the system is used. Press Red button on startup to load default values

 Things to do:
 1. programme up a decent clock setting routine
 2. interface the system to control DSLR Remote Pro to control ISO ramping

 This program is free software: you can redistribute it
 and/or modify it under the terms of the GNU General
 Public License as published by the Free Software
 Foundation, either version 3 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the
 implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE. See the GNU General Public License
 for more details.

 The core of the non blocking software is based on code
 supplied by Shutterdrone on the OpenMoco website under
 the terms of the GNU General Public License.
 */

// Libraries used
#include <LiquidCrystal.h> // library from LCD display
#include <MsTimer2.h>      // library for interrupt control
#include "Wire.h"          // used with DS1307
#include <stdlib.h>        // string manipulation etc
#include <EEPROM.h>        // used to save variables to eeprom

// Camera pin definitions
#define SHUTTER_PIN 22     // digital output for shutter
#define FOCUS_PIN   23     // digital output for focus tap
#define PCSYNC_PIN  24     // digital input for pcsync confirmation - not implimented yet
#define CAMLED_PIN  25     // digital output for "shutter fired" LED

// Button definitions
#define B_LT 49                      // Left button
#define B_RT 50                      // Right button
#define B_UP 51                      // Up button
#define B_DN 52                      // Down button
#define B_CT 53                      // Centre button

unsigned long button_hit = 0;        // time last button was hit

// define battery status variables
int       batteryPin = 0;            // analogue pin 0 hard wired to battery status line
float     batteryValue;              // true battery voltage
int       batteryPercent;            // % full against max reference voltage
float     batteryMaxVolts = 4.15;    // max reference voltage (full charge)
float     batteryMinVolts = 3.35;    // min battery voltage (low alarm at batteryMinVolts + 0.1v)
float     batteryAverage = 0;        // the average voltage

// variables used to smooth the voltage reading
const int numReadings = 20;          // no. of samples for voltage smoothing
int       readings[numReadings];     // the readings from the analog input
int       index = 0;                 // the index of the current reading
int       total = 0;                 // the running total

// assign other variables
char* clearline = "                    "; // empty line string

// buffer for string conversion
char  buffer_Msg[] = "123456578901234567890";

// main display strings
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(31, 33, 35, 37, 39, 41);

#define   lcd_LIGHT 29            // digital pin controlling LCD backlight
boolean   lcd_SW;                 // flag for controlling backlight status
int       lcd_TIMEOUT = 30000;    // timeout delay before screen turns off
float     lcd_TIMER;              // Backlight timer used with millis()
int       lcd_LAST_UPDATE = 200;  // Only update display every 200 ms
float     lcd_UPDATE_TM = 0;      // variable to check against millis

// arrays used to build custom characters
byte UP[8] = // array to make an arrow pointing UP
{
  B00000,
  B00100,
  B01110,
  B11111,
  B00100,
  B00100,
  B00100,
  B00000
};
byte DOWN[8] = // array to make an arrow pointing DOWN
{
  B00000,
  B00100,
  B00100,
  B00100,
  B11111,
  B01110,
  B00100,
  B00000
};
byte LEFT[8] = // array to make an arrow pointing LEFT
{
  B00000,
  B00000,
  B00100,
  B01100,
  B11111,
  B01100,
  B00100,
  B00000
};
byte RIGHT[8] = // array to make an arrow pointing RIGHT
{
  B00000,
  B00000,
  B00100,
  B00110,
  B11111,
  B00110,
  B00100,
  B00000
};

/*
LiquidCrystal KEYWORDS
 #######################################
 # Methods and Functions
 #######################################

 begin
 clear
 home
 print
 setCursor
 cursor
 noCursor
 blink
 noBlink
 display
 noDisplay
 autoscroll
 noAutoscroll
 leftToRight
 rightToLeft
 scrollDisplayLeft
 scrollDisplayRight
 createChar
 */

// setup RTC
#define DS1307_I2C_ADDRESS 0x68

// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
  return ( (val/10*16) + (val%10) );
}

// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
  return ( (val/16*10) + (val%16) );
}

// 1) Sets the date and time on the ds1307
// 2) Starts the clock
// 3) Sets hour mode to 24 hour clock
// Assumes you're passing in valid numbers

void setDateDs1307 (
byte second,        // 0-59
byte minute,        // 0-59
byte hour,          // 1-23
byte dayOfWeek,     // 1-7
byte dayOfMonth,    // 1-28/29/30/31
byte month,         // 1-12
byte year)          // 0-99

{
  Wire.beginTransmission(DS1307_I2C_ADDRESS);
  Wire.send(0);
  Wire.send(decToBcd(second));    // 0 to bit 7 starts the clock
  Wire.send(decToBcd(minute));
  Wire.send(decToBcd(hour));      // If you want 12 hour am/pm you need to set
  // bit 6 (also need to change readDateDs1307)
  Wire.send(decToBcd(dayOfWeek));
  Wire.send(decToBcd(dayOfMonth));
  Wire.send(decToBcd(month));
  Wire.send(decToBcd(year));
  Wire.endTransmission();
}

// Gets the date and time from the ds1307
void getDateDs1307(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)

{
  // Reset the register pointer
  Wire.beginTransmission(DS1307_I2C_ADDRESS);
  Wire.send(0);
  Wire.endTransmission();

  Wire.requestFrom(DS1307_I2C_ADDRESS, 7);

  // A few of these need masks because certain bits are control bits
  *second     = bcdToDec(Wire.receive() & 0x7f);
  *minute     = bcdToDec(Wire.receive());
  *hour       = bcdToDec(Wire.receive() & 0x3f);  // Need to change this if 12 hour am/pm
  *dayOfWeek  = bcdToDec(Wire.receive());
  *dayOfMonth = bcdToDec(Wire.receive());
  *month      = bcdToDec(Wire.receive());
  *year       = bcdToDec(Wire.receive());
}

// functions for EEPROM memory read/write
template <class T> int EEPROM_writeAnything(int ee, const T& value)
{
  const byte* p = (const byte*)(const void*)&value;
  int mem;
  for (mem = 0; mem < sizeof(value); mem++)
    EEPROM.write(ee++, *p++);
  return mem;
}

template <class T> int EEPROM_readAnything(int ee, T& value)
{
  byte* p = (byte*)(void*)&value;
  int mem;
  for (mem= 0; mem < sizeof(value); mem++)
    *p++ = EEPROM.read(ee++);
  return mem;
}

// Camera function variables
long          INTERVAL_TM = 1200;              // default interval delay between exposures
int           MIN_INTERVAL_TM = 100;           // minimum interval delay

long          EXPOSURE_TM = 60;                // default exposure length (how long to fire camera)
int           MIN_EXPOSURE_TM = 60;            // minimum exposure (pulse length)

float         EXPOSURE_INC = 1.05;             // percentage to increase or decrease exposure
float         EXPOSURE_DEC = 0.95;

unsigned long LAST_TM = 0;                     // last time camera fired
long          SHOT_COUNT = 0;                  // running total of photographs taken
boolean       SEQ_RUNNING = false;             // is the camera running a sequence
boolean       EXPOSING = false;                // whether or not we're currently exposing

// values to read/write to Eeprom, rememeber they are "configuration.INTERVAL_TM" etc.
// they are not the same as INTERVAL_TM etc
struct config_t
{
  long INTERVAL_TM;
  long EXPOSURE_TM;
}
configuration;

void setup() {

  // initialise all battery array readings to 0
  for (int thisReading = 0; thisReading < numReadings; thisReading++)
    readings[thisReading] = 0;

  // preload battery arrays with a cycle through the analogue readings
  // to avoid silly numbers appearing on the initial display
  int temp;
  for (temp =0; temp <= numReadings; temp++){
    get_Battery();
  }

  // setup pins for input/output
  pinMode(SHUTTER_PIN, OUTPUT);
  pinMode(FOCUS_PIN, OUTPUT);
  pinMode(PCSYNC_PIN, INPUT);
  pinMode(CAMLED_PIN, OUTPUT);

  // set input button pins
  pinMode(B_LT, INPUT);  // Left
  pinMode(B_RT, INPUT);  // Right
  pinMode(B_UP, INPUT);  // Up
  pinMode(B_DN, INPUT);  // Down
  pinMode(B_CT, INPUT);  // Centre

  // setup display
  pinMode (lcd_LIGHT, OUTPUT);
  digitalWrite(lcd_LIGHT, HIGH);
  lcd_SW = HIGH;        // turn on LCD backlight
  lcd_TIMER = millis(); // set timer start

  // set up the LCD's number of rows and columns:
  // Note 4 rows from 0 to 3
  lcd.begin(20, 4);

  // creates custom characters from the arrays storing them to
  // one of 8 (0-7) available custom character slots.
  lcd.createChar(0, UP);
  lcd.createChar(1, DOWN);
  lcd.createChar(2, LEFT);
  lcd.createChar(3, RIGHT);

  /* use lcd.write() for custom Arrow characters
   lcd.write(0);     UP
   lcd.write(1);     DOWN
   lcd.write(2);     LEFT
   lcd.write(3);     RIGHT
   */

  // display intro screen
  lcd.clear();
  lcd.setCursor(4, 0);
  lcd.print("Canon Camera");
  lcd.setCursor(5, 1);
  lcd.print("Controller");
  lcd.setCursor(8, 2);
  lcd.print("C^3");
  lcd.setCursor(4, 3);
  lcd.print("version  1.6");
  delay(2000);
  lcd.clear();

  // check if Centre button is pressed on startup.
  // If LOW load values from memory (default is to load memory values)
  // if HIGH load default values
  if( digitalRead(B_CT) == LOW)
  {
    EEPROM_readAnything(0, configuration);
    INTERVAL_TM = configuration.INTERVAL_TM;
    EXPOSURE_TM = configuration.EXPOSURE_TM;

    lcd.setCursor(0, 1);
    lcd.print(" Saved data loaded");
    lcd.setCursor(0, 2);
    lcd.print("Press FIRE to reset");
    delay(1000);
    lcd.clear();
  }
  else
  {
    lcd.setCursor(0, 1);
    lcd.print("Default data loaded");
    delay(1000);
    lcd.clear();
  }

  // display main screen
  lcd.setCursor(0, 0);
  lcd.print("Interval = ");
  lcd.setCursor(18, 0);
  lcd.write(0);                // UP arrow
  lcd.write(1);                // DOWN arrow

  lcd.setCursor(0, 1);
  lcd.print("Exposure = ");
  lcd.setCursor(18, 1);
  lcd.write(2);                // LEFT arrow
  lcd.write(3);                // RIGHT arrow

  lcd.setCursor(0,2);
  lcd.print("Shot     = ");

  // setup DS1307 (need to convert this into a proper clocking setting routine)
  byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
  Wire.begin();
  // Change these values to what you want to set your clock to.
  // You probably only want to set your clock once and then remove
  // the setDateDs1307 call.
  // unrem next lines to set clock
  /*
   second = 0;
   minute = 25;
   hour = 12;
   dayOfWeek = 4;
   dayOfMonth = 20;
   month = 5;
   year = 10;
   setDateDs1307(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
   */
}

void loop() {

  if (SEQ_RUNNING == true) {
    shutter_control();
  }
  user_input();
}

battery_status.pde

/*
check battery status and work out percentage life left in the lithium battery
*/

// Battery Status
void get_Battery()  {
  // read the value from the battery sensor analogue pin (full charge voltage = batteryMaxVolts)
  // create a rolling average over 20 samples

  // subtract the last reading:
  total = total - readings[index];

  // read from the sensor:
  readings[index] = analogRead(batteryPin);

  // add the reading to the total:
  total = total + readings[index];
  // advance to the next position in the array:
  index = index + 1;

  // if we're at the end of the array...
  if (index >= numReadings)
    // ...wrap around to the beginning:
    index = 0;

  // calculate the average:
  batteryAverage = total / numReadings;

  // convert to volts
  batteryValue = (batteryAverage/1024)*5; // 1024 = 5 volts
  batteryPercent = ((batteryValue-batteryMinVolts)/(batteryMaxVolts-batteryMinVolts))*100;
}

lcd_display.pde

/*
Update display every lcd_UPDATE_TM ms
*/

void update_LCD() {

  // only update every lcd_UPDATE_TM ms
  if (millis() - lcd_UPDATE_TM > lcd_LAST_UPDATE) {

    // update display
    lcd.setCursor(11, 0);
    lcd.print("     ");
    lcd.setCursor(11, 0);
    lcd.print(INTERVAL_TM);

    lcd.setCursor(11, 1);
    lcd.print("     ");
    lcd.setCursor(11, 1);
    lcd.print(EXPOSURE_TM);

    lcd.setCursor(11, 2);
    lcd.print("     ");
    lcd.setCursor(11, 2);
    lcd.print(SHOT_COUNT);

    // update status line with time and battery
    // Update clock on status line
    get_Clock();

    // update Battery on status line
    get_Battery();
    lcd.setCursor(11, 3);
    lcd.print("B     ");
    lcd.setCursor(12, 3);
    lcd.print(batteryPercent);
    lcd.print("%");

    // check battery status and display label
    if (batteryPercent <= 5){
      lcd.setCursor(17,3);
      lcd.print("LOW");
    }
    else
    {
      lcd.setCursor(17,3);
      lcd.print(" OK");
    }
    // update update timer
    lcd_UPDATE_TM = millis();
  }
}

rtc_status.pde

/*
get update from RTC and display it
*/

void get_Clock()
{
  byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;

  getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);

  lcd.setCursor(0,3);
  if (hour < 10){
    lcd.print("0");
  }
  lcd.print(hour, DEC);
  lcd.print(":");
  if (minute < 10){
    lcd.print("0");
  }
  lcd.print(minute, DEC);
  lcd.print(":");
  if (second < 10){
    lcd.print("0");
  }
  lcd.print(second, DEC);
}

shutter_control.pde

/*
control the camera and the interrupt
*/

void shutter_control() {

  // if the camera is not currently exposing, and our
  // timer has elapsed, fire camera...

  if( EXPOSING == false && millis() - LAST_TM > INTERVAL_TM ) {

    // set timer interrupt to call our
    // function to disengage the camera
    MsTimer2::set( EXPOSURE_TM, shutter_off );

    // enable timer
    MsTimer2::start();

    // set flag to indicate that we're currently
    // exposing

    EXPOSING = true;

    // enable optocoupler and LED
    digitalWrite(SHUTTER_PIN, HIGH);
    digitalWrite(CAMLED_PIN, HIGH);

    SHOT_COUNT++;

  } // end if not exposing and timer has elapsed
}

void shutter_off() {

  // disable optocoupler and LED
  digitalWrite(SHUTTER_PIN, LOW);
  digitalWrite(CAMLED_PIN, LOW);

  // disable timer
  MsTimer2::stop();

  // we set this now to ensure that our
  // interval time is measured from the
  // time an image is completed until the
  // time the next one is triggered
  //
  // if you want the interval time to be
  // measured between the time the image
  // is triggered and the next image is
  // triggered, move this to before
  // bringing the camera pin high

  LAST_TM = millis();

  // reset exposing flag
  EXPOSING = false;
}

user_input.pde

/*
 user input functions
 */

void user_input() {
  // this function determines if any button has been hit,
  // and what to do about it
  // up and down do not require a HIGH (unpressed) reading
  // so that you can hold them down and quickly cycle through
  // values (about 3 changes per second)

  update_LCD();

  // check to turn off backlight
  if (lcd_SW == HIGH){

    if (lcd_TIMER + lcd_TIMEOUT < millis()) {
      lcd_SW = LOW;
      digitalWrite(lcd_LIGHT, LOW);
    }
  }

  // check for button press
  if( digitalRead(B_UP) == HIGH && millis() - button_hit > 300) {

    if (lcd_SW == LOW && millis() - button_hit > 300) {
      lcd_ON();
      button_hit = millis();
      return;
    }
    else
    {
      take_button_action(0);
      button_hit = millis();
      return;
    }
  }

  if( digitalRead(B_DN) == HIGH && millis() - button_hit > 300 ) {
    //pre_dn = true;

    lcd_ON();
    take_button_action(1);
    button_hit = millis();
    return;
  }

  if( digitalRead(B_LT) == HIGH && millis() - button_hit > 300 ) {

    if (lcd_SW == LOW && millis() - button_hit > 300) {
      lcd_ON();
      button_hit = millis();
      return;
    }
    else
    {

      take_button_action(2);
      button_hit = millis();
      return;
    }
  }

  if( digitalRead(B_RT) == HIGH && millis() - button_hit > 300) {

    if (lcd_SW == LOW && millis() - button_hit > 300) {
      lcd_ON();
      button_hit = millis();
      return;
    }
    else
    {
      take_button_action(3);
      button_hit = millis();
      return;
    }
  }

  if( digitalRead(B_CT) == HIGH && millis() - button_hit > 300) {

    if (lcd_SW == LOW && millis() - button_hit > 300) {
      lcd_ON();
      button_hit = millis();
      return;
    }
    else
    {
      take_button_action(4);
      button_hit = millis();
      return;
    }
  }
}

void take_button_action( byte button ) {

  switch(button) {
  case 0:
    // up hit
    INTERVAL_TM = INTERVAL_TM + 100;
    // update memory
    configuration.INTERVAL_TM = INTERVAL_TM;
    EEPROM_writeAnything(0, configuration);
    update_LCD();
    return;
    break;

  case 1:
    // down hit
    INTERVAL_TM = INTERVAL_TM - 100;
    if (INTERVAL_TM <= MIN_INTERVAL_TM) {
      INTERVAL_TM = MIN_INTERVAL_TM;
    }
    update_LCD();
    // update memory
    configuration.INTERVAL_TM = INTERVAL_TM;
    EEPROM_writeAnything(0, configuration);
    return;
    break;

  case 2:
    // left hit - reduce exposure
    EXPOSURE_TM = int(EXPOSURE_TM * EXPOSURE_DEC);
    if (EXPOSURE_TM <= MIN_EXPOSURE_TM) {
      EXPOSURE_TM = MIN_EXPOSURE_TM;
    }
    update_LCD();
    // update memory
    configuration.EXPOSURE_TM = EXPOSURE_TM;
    EEPROM_writeAnything(0, configuration);
    return;
    break;

  case 3:
    // right hit - increase exposure
    EXPOSURE_TM = int(EXPOSURE_TM * EXPOSURE_INC);
    // update memory
    configuration.EXPOSURE_TM = EXPOSURE_TM;
    EEPROM_writeAnything(0, configuration);
    update_LCD();
    return;
    break;

  case 4:
    // centre hit

    if (SEQ_RUNNING == true) {
      SEQ_RUNNING = false;
      update_LCD();
    }
    else {
      SEQ_RUNNING = true;
      update_LCD();
    }
    return;
    break;
  }
}

// function to turn on lcd_LIGHT and reset timer
void lcd_ON(){
  lcd_TIMER = millis(); // reset lcd timer
  digitalWrite(lcd_LIGHT, HIGH);
  lcd_SW = HIGH;
}

This entry was posted in PhotosbyKev. Bookmark the permalink.

9 Responses to Camera Intervalometer Source Code

  1. Pingback: C-Cubed – the Arduino-based camera intervalometer « freetronicsblog

  2. Kevin Johnson says:

    Kev to Kev

    Any plans on selling built units? Now that Little Bramper is sold out, I’m sure there would be a market for these in the timelapse community. Not everyone has the knowledge, patience and time to do this themselves. I would be willing to pay several hundred dollars for a unit that I knew worked.

  3. Kevin Merisnky says:

    First off thank you so much for documenting this, I’m going to try and build a version of this but I’m not very good with code. When trying to upload I get an error, it revolves around defining the battery status variables. The code looks like this;

    unsigned long button_hit = 0; // time last button was hit

    // define battery status variables
    int batteryPin = 0; // analogue pin 0 hard wired to battery status line
    float batteryValue; // true battery voltage
    int batteryPercent; // % full against max reference voltage
    float batteryMaxVolts = 4.15; // max reference voltage (full charge)
    float batteryMinVolts = 3.35; // min battery voltage (low alarm at batteryMinVolts + 0.1v)
    float batteryAverage = 0; // the average voltage

    I’m thinking that I need to enter the specs on the battery I’m using. Two problems, I don’t know how and I’m currently powering off of USB until the system is much further along. PLEASE HELP!!!

  4. Kayvan says:

    Hello everybody,

    I want to use SHT10 sensor to read temperature and humidity and the display them on LCD 2*16 via PIC18F452.

    Anybody that have the code,please put it on this page,i will pick up that.

    Many thanks,

    Kayvan

  5. Martin Clark says:

    i was wondering if you could get into contact with me on your arduino controller, i wish this or something like this to be my first project, and i was wondering if i could just ask you some questions.

    its more theory rather than arduino code…. thanks

    Martieuk [at] hotmail [dot] com

  6. Terry says:

    Hi,
    Great looking device you have there. I am getting a bit jealous.
    I am taking too the challenge, but I have some question concerning the light measurement. I notice that you used the TSL230R as a incident light meter. Did you change Church’s initial code?
    Also, I am having some issue on where to aim the sensor for sunset. Do you have any advice?
    Thanks again.
    T

    • photosbykev says:

      Hi,

      the main core of the original code is unchanged, all I’ve done is play around at the edges of it to suit the devices I was using. It’s possible to apply an Ev offset in the programme so you can use the light sensor for incident or reflective readings. Measuring off a card or through a diffuser would give you the best average for metering and the direction of the reading becomes less important. i.e I visually correct the base value to give me a good exposure histogram and then let the system run.

      regards
      Kev

  7. Pingback: Arduino intervalometer and bulb ramping for Digital Cameras | PhotosbyKev

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>