Nikon CLS Slave Flash Trigger
This small project is a slave flash, which can trigger up to 4 camera flashes (non-TTL mode). It is meant for use with the Nikon CLS system, although it can probably work with other brands as well, as long as the protocol heuristics are similar.
The heuristics of the Nikon CLS system are simple: there are a number of short pre-flashes, then a delay, then the main flash. The algorithm I am using basically waits until it sees a flash, reads in any number of flashes which occur with less than the designated time between them, and when another flash happens which is between the specified min and max times, it fires the slave flashes. The Digital Photography Tips and Technique blog wrote an article on Nikon CLS Advanced Wireless Lighting, as well as an implementation of a slave flash using the Arduino. I used these resources heavily for understanding the protocol, although I chose to use a plain AVR instead of an Arduino for this project.
The Schematics
The schematics are simple. I use an AVR (I am currently using an ATmega168, but in theory this should be portable to most modern AVRs with sufficient memory), with a status LED, photo diode (used in photo voltaic mode), and a few triacs for the actual trigger:
I am using a Sharp BS520 for the photo diode, and a Philips BT131 series triac, although similar products could easily be used as well.
The Code
The code is very simple. I use define statements for all threshold constants and port / pin definitions. I use my own libraries for the analog readings and the time counting; I plan to release these eventually, but they are not yet on my website; please feel free to email me with questions or if you would like me to send them to you.
#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
#include "lib/analog/analog.h"
#include "lib/timer/timer.h"
#include "lib/serial/serial.h"
//Status LED
#define STATUS_LED_PORT PORTD
#define STATUS_LED_PIN PIND5
//Analog pin
#define ANALOG_PIN 0
//XSYNC pin
#define XSYNC_PORT PORTB
#define XSYNC_PIN PINB1
//The amount of delta required to register a flash.
#define FLASH_THRESHOLD 40
//The amount of delta required to re-calibrate ambient
#define AMBIENT_THRESHOLD -10
//The time after the last command pulse before the 'fire' command happens; in milli seconds.
#define MIN_FIRE_PULSE_DELAY 45
#define MAX_FIRE_PULSE_DELAY 100
//The maximum length of a pulse. If we read one longer than this, then we will re-adjust
// ambient, as it is likely just than someone has turned the lights on.
#define MAX_PULSE_WIDTH 500
//The maximum count of pulses. If we exceed this, we will re-adjust ambient, as it is likely
// that we are just sitting on the edge of the flash threshold
#define MAX_PULSE_COUNT 20
//#define DEBUG
uint16_t analog = 0; //Analog input
uint16_t ambient = 0; //Ambient light. Will be adjusted over time.
int16_t flash = 0; //Delta between ambient and current
uint8_t pulse_count = 0; //How many pulses have happened so far
uint64_t status_time = 0; //Start time of the current pulse
uint64_t pulse_end_time = 0; //End time of the last pulse
uint64_t time = 0; //Current time (calculated at start of loop)
#ifdef DEBUG
char temp[64];
#endif
static inline void fire_flash(){
XSYNC_PORT |= _BV(XSYNC_PIN);
_delay_ms(1);
XSYNC_PORT &= ~_BV(XSYNC_PIN);
//Pulse status 10 times to confirm flash.
for (uint8_t i = 0; i <= 10; i++){
STATUS_LED_PORT |= _BV(STATUS_LED_PIN);
_delay_ms(5);
STATUS_LED_PORT &= ~_BV(STATUS_LED_PIN);
_delay_ms(20);
}
_delay_ms(100);
}
static inline void read_flash(){
//Current analog value
analog_read_a(&analog);
flash = analog - ambient;
}
static inline void read_ambient(){
uint32_t sum = 0;
for (uint8_t i = 0; i < 16; i++){
analog_read_a(&ambient);
_delay_ms(1);
sum += ambient;
}
//Divide by 16 (2^4)
ambient = (sum >> 4);
#ifdef DEBUG
sprintf(temp, "Adjusted ambient to %d\n\r", ambient);
serial_write_s(temp);
#endif
}
static inline void reset(){
//Reset everything
pulse_count = 0;
time = timer_millis();
read_flash();
}
int main (void){
#ifdef DEBUG
serial_init_b(9600);
#endif
//Analog init
uint8_t pins[1];
pins[0] = ANALOG_PIN;
analog_init(pins, 1, ANALOG_INTERNAL);
//Timer init
timer_init();
//Get initial reading to figure out ambient light; take the average of a number
// of readings
read_ambient();
//Enable XSYNC and LED in output mode
*(&XSYNC_PORT - 0x1) |= _BV(XSYNC_PIN);
*(&STATUS_LED_PORT - 0x1) |= _BV(STATUS_LED_PIN);
//Flash LED five times to confirm we are up and running
for (uint8_t i = 0; i <= 5; i++){
STATUS_LED_PORT |= _BV(STATUS_LED_PIN);
_delay_ms(10);
STATUS_LED_PORT &= ~_BV(STATUS_LED_PIN);
_delay_ms(50);
}
//Main program loop
while (1){
//Current time in milliseconds
time = timer_millis();
#ifdef DEBUG
//Print status / heartbeat every second
if (time - status_time > 1000000){
sprintf(temp, "Ambient: %d; Analog: %d; Flash: %d\n\r", ambient, analog, flash);
serial_write_s(temp);
status_time = time;
}
#endif
read_flash();
//Start of a new pulse after a delay of more than 40ms but less than 100ms; this is a fire pulse!
if (pulse_count > 0
&& flash > FLASH_THRESHOLD
&& time - pulse_end_time > MIN_FIRE_PULSE_DELAY
&& time - pulse_end_time < MAX_FIRE_PULSE_DELAY){
fire_flash();
reset();
}
//If the last pulse end time was too long ago, reset.
if (pulse_count > 0
&& time - pulse_end_time > MAX_FIRE_PULSE_DELAY){
#ifdef DEBUG
serial_write_s("Too long since last pulse...\n\r");
#endif
reset();
}
//If the analog read value is sufficiently less than ambient, re-read ambient.
if (flash < AMBIENT_THRESHOLD
|| pulse_count > MAX_PULSE_COUNT){
#ifdef DEBUG
serial_write_s("Flash is negative...\n\r");
#endif
read_ambient();
}
//Start of a new pulse within less than 40ms or more than 100ms (i.e. not a fire pulse)
if (flash > FLASH_THRESHOLD){
//Read to the end of the pulse. If the pulse is lasting longer than MAX_PULSE_WIDTH
// then we may need to adjust the ambient value.
while (flash > FLASH_THRESHOLD
&& timer_millis() - time < MAX_PULSE_WIDTH){
read_flash();
}
#ifdef DEBUG
sprintf(temp, "Last pulse ended %8ld us ago\n\r", (uint32_t) (timer_millis() - pulse_end_time));
serial_write_s(temp);
#endif
pulse_end_time = timer_millis();
//If this 'pulse' was really just ambient, adjust ambient and reset...
if (timer_millis() - time > MAX_PULSE_WIDTH){
#ifdef DEBUG
serial_write_s("Pulse time too long; probably just ambient...\n\r");
#endif
read_ambient();
reset();
}
//Otherwise, increment pulse count
else {
pulse_count++;
#ifdef DEBUG
serial_write_s("Incremented pulse count...\n\r");
#endif
}
}
}
}
End Result
I put the entire thing into a small enclosure, on a breadboard (I love breadboards within enclosures -- it gives the professional look of a real enclosure, with the ability to change things that is inherent with breadboards!) There is a power plug and regulator in case I want to plug it into the wall; I also plan on including a small battery to power it eventually. The LED and photo diode on are on the front, while the flash connections are on the back (I use RCA connectors for this, along with modified flashes with RCA sync inputs).