Camera slider with eased moves on an Arduino Nano
A NEMA 17 on an A4988 homes to an end switch, then makes a video move that speeds up and slows down evenly, or a timelapse that moves, settles and fires the shutter. The whole build, an Arduino Nano and 9 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Motorized camera slider on an Arduino Nano: a NEMA 17 on an A4988 pulls the carriage along a
// belt, easing in and out so the footage has no jolt at either end, with a
// timelapse mode that moves, settles and fires the shutter in steps.
//
// Wiring.
//
// A4988 STEP 3, DIR 4, RST linked to SLP, VDD 5 V, VMOT 12 V
// home end switch on pin 2 (COM to GND, NO to the pin)
// GO pushbutton on A3 mode slide switch on A2 (both read as digital pins)
// shutter pin 13 (an LED here; an optocoupler to the camera's remote socket)
// knobs A0 move time, A1 ease
//
// In Mokxi: press Run and the carriage homes towards the end switch: click
// the switch to say it has arrived (with no click it stops after the whole
// rail and calls that home). Set the knobs, then press GO. Slide the mode
// switch for a timelapse. The Serial Monitor shows each move.
//
// The ease. A move that starts at full speed jerks the camera and shows as a
// wobble in the shot. Here the carriage speeds up evenly for the first part
// of the move, cruises, and slows evenly for the same time at the end: a
// trapezoid of speed. The ease knob sets how much of the move is spent
// speeding up (and as much slowing down), from none to half each way.
//
// Steps per millimeter. A GT2 belt on a 20-tooth pulley moves 40 mm a turn,
// and a 200-step motor in full steps makes that 5 steps a millimeter. The
// driver here runs in full steps (MS1 to MS3 left open) so the sim stays
// quick; on a real slider set 1/16 microstepping for smoothness and multiply
// STEPS_PER_MM by 16.
//
// Not simulated: the camera. Pin 13 lights an LED for each shot; on the rail it
// would drive an optocoupler across the shutter contacts of the camera's
// wired remote, which keeps the camera's circuit apart from the Arduino's.
// ---------------------------------------------------------------------------
// Make it yours
// ---------------------------------------------------------------------------
const int STEP_PIN = 3;
const int DIR_PIN = 4;
const int HOME_PIN = 2;
const int GO_PIN = A3;
const int MODE_PIN = A2;
const int SHUTTER_PIN = 13;
const int TIME_KNOB = A0;
const int EASE_KNOB = A1;
const long STEPS_PER_MM = 5; // GT2 belt, 20-tooth pulley, full steps
const long RAIL_MM = 300; // usable travel
const bool HOME_IS_LOW_DIR = true; // which way DIR points at the home switch
// The move-time knob's range, in seconds. With the most ease the top speed
// is twice the average, and a stepper that is asked for more steps a second
// than it can follow skips them: 1500 steps in 4 s peaks at 750 a second,
// inside what this motor follows. Shorten the time only with less ease.
const long MIN_MOVE_S = 4;
const long MAX_MOVE_S = 30;
// Timelapse.
const int SHOTS = 10; // photos along the rail
const unsigned long SETTLE_MS = 300; // let the rail stop shaking before a shot
const unsigned long SHUTTER_MS = 150; // how long the shutter line is held
const unsigned int HOMING_US = 2000; // step period while homing (slow)
// ---------------------------------------------------------------------------
long position = 0; // steps from home
const long RAIL_STEPS = RAIL_MM * STEPS_PER_MM;
void stepOnce(bool away) {
digitalWrite(DIR_PIN, (away == HOME_IS_LOW_DIR) ? HIGH : LOW);
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(3);
digitalWrite(STEP_PIN, LOW);
position += away ? 1 : -1;
}
void home() {
Serial.println("Homing...");
for (long i = 0; i < RAIL_STEPS; i++) {
if (digitalRead(HOME_PIN) == LOW) {
Serial.println("Home switch hit");
position = 0;
return;
}
stepOnce(false);
delayMicroseconds(HOMING_US);
}
Serial.println("No home switch: calling here home");
position = 0;
}
// Fraction of the distance covered at fraction u of the time, both in
// thousandths, for a trapezoid of speed that spends a thousandths speeding up
// and as long slowing down. Answers in ten-thousandths.
long eased(long u, long a) {
if (a <= 0) return u * 10;
if (u <= a) return (u * u * 100 / (2 * a)) * 100 / (1000 - a);
if (u >= 1000 - a) return 10000 - eased(1000 - u, a);
return (u - a / 2) * 10000 / (1000 - a);
}
// Move to `target` steps from home in `ms` milliseconds, eased.
void moveTo(long target, unsigned long ms, long ease) {
long start = position;
long distance = target - start;
unsigned long t0 = millis();
while (true) {
unsigned long t = millis() - t0;
// time gone in ten-thousandths, and the curve read between thousandths,
// so the target moves smoothly rather than in bursts of steps
long u10 = t >= ms ? 10000 : (long)(t * 10000 / ms);
long u = u10 / 10;
long e = eased(u, ease);
if (u < 1000) e += (eased(u + 1, ease) - e) * (u10 % 10) / 10;
long want = start + distance * e / 10000;
if (position < want) {
stepOnce(true);
} else if (position > want) {
stepOnce(false);
} else if (u >= 1000) {
return;
}
delayMicroseconds(200); // the top step rate this loop allows
}
}
void shoot(int n) {
delay(SETTLE_MS);
digitalWrite(SHUTTER_PIN, HIGH);
delay(SHUTTER_MS);
digitalWrite(SHUTTER_PIN, LOW);
Serial.print("Shot ");
Serial.print(n);
Serial.print(" at ");
Serial.print(position / STEPS_PER_MM);
Serial.println(" mm");
}
void setup() {
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(SHUTTER_PIN, OUTPUT);
pinMode(HOME_PIN, INPUT_PULLUP);
pinMode(GO_PIN, INPUT_PULLUP);
pinMode(MODE_PIN, INPUT_PULLUP);
Serial.begin(115200);
Serial.println("Camera slider");
home();
Serial.println("Ready: set the knobs and press GO");
}
void loop() {
if (digitalRead(GO_PIN) != LOW) {
return;
}
long seconds = MIN_MOVE_S + (long)analogRead(TIME_KNOB) * (MAX_MOVE_S - MIN_MOVE_S) / 1023;
long ease = (long)analogRead(EASE_KNOB) * 500 / 1023; // 0 to half the move
long target = position < RAIL_STEPS / 2 ? RAIL_STEPS : 0;
bool timelapse = digitalRead(MODE_PIN) == LOW;
if (!timelapse) {
Serial.print("Video move to ");
Serial.print(target / STEPS_PER_MM);
Serial.print(" mm in ");
Serial.print(seconds);
Serial.print(" s, ease ");
Serial.print(ease / 10);
Serial.println("%");
moveTo(target, (unsigned long)seconds * 1000, ease);
} else {
Serial.print("Timelapse: ");
Serial.print(SHOTS);
Serial.println(" shots");
long start = position;
shoot(1);
for (int n = 1; n < SHOTS; n++) {
long next = start + (target - start) * n / (SHOTS - 1);
moveTo(next, (unsigned long)seconds * 1000 / (SHOTS - 1), ease);
shoot(n + 1);
}
}
Serial.print("Done at ");
Serial.print(position / STEPS_PER_MM);
Serial.println(" mm");
while (digitalRead(GO_PIN) == LOW) {
}
}
Parts list
12 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
19 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: Arduino Nano pin GND; Limit switch, lever microswitch pin COM; Stepper driver, A4988 pin GNDM; Stepper driver, A4988 pin GND
- Arduino Nano pin 2; Limit switch, lever microswitch pin NO
- Arduino Nano pin 3; Stepper driver, A4988 pin STEP
- Arduino Nano pin 4; Stepper driver, A4988 pin DIR
- Ground: Arduino Nano pin GND; Resistor, 220 Ω pin 2; Potentiometer (1) pin 1; Potentiometer (2) pin 1; Pushbutton pin 2a; Slide switch pin 3
- Arduino Nano pin 5V; Potentiometer (1) pin 3; Potentiometer (2) pin 3; Stepper driver, A4988 pin VDD
- Arduino Nano pin A3; Pushbutton pin 1a
- Arduino Nano pin A2; Slide switch pin 2
- Arduino Nano pin A1; Potentiometer (2) pin 2
- Arduino Nano pin A0; Potentiometer (1) pin 2
- Arduino Nano pin 13; LED, white pin A
- LED, white pin C; Resistor, 220 Ω pin 1
- Ground: Stepper driver, A4988 pin MS1; Stepper driver, A4988 pin MS2; Stepper driver, A4988 pin MS3
- Stepper driver, A4988 pin RST; Stepper driver, A4988 pin SLP
- 12 V: Stepper driver, A4988 pin VMOT
- Stepper driver, A4988 pin 2B; Stepper motor, NEMA 17 pin A1
- Stepper driver, A4988 pin 2A; Stepper motor, NEMA 17 pin A2
- Stepper driver, A4988 pin 1A; Stepper motor, NEMA 17 pin B1
- Stepper driver, A4988 pin 1B; Stepper motor, NEMA 17 pin B2
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.