Motion Control With Arduino: Motorising a Camera Slider
The availablility of cheap stepper motors and drivers these days allows ample opportunity for experimenting outside of the more expensive and complicated 2D/3D cutting and printing projects.
For this project, I'll take the OpenBuilds camera slider (refer to the build video in Building a Basic Video Slider With Open Source CNC Parts) and motorise it. I’ll also create a self-contained system for controlling the motor.
This tutorial specifically covers putting together the hardware, but primarily building a rudimentary 16x2 LCD GUI using the LiquidCrystal library and a simple menu system for it to display, followed by the workings of of the A4988 stepper driver and how to control it with Arduino.
This project is heavy on loops and steps, and while overall the project is more intermediate, I've tried to explain it in such a way that beginners can be up and running relatively quickly.
Equipment List
Components
- Arduino Uno
- LCD Keypad Shield or separate 16x2 LCD and buttons if you know how to make that work
- Pololu A4988 [Black Edition] Stepper Driver
- Small aluminium self-stick heat sink
- Breadboards, male and female jumper wires, etc
- 220-330 ohm resistor (1/4W will probably do), standard NPN transistor (I used a BC109)
- 3.5mm stereo TRS socket
- 3.5mm to 2.5mm stereo TRS adaptor cable
- 3.5mm extension cable as necessary for the slider length
- 9V barrel jack power supply if you want to take the Arduino away from the computer's USB power
- 12V 2A power supply to run the stepper motor
- NEMA 17 stepper motor
Parts
- GT2 5mm-wide, 2mm-pitch timing belt: double the slider length plus a foot for safety (11 feet for me)
- Smooth idler pulley kit
- Belt-tension torsion spring if you have difficulty maintaining belt tension over a long run
- 2x Belt crimp clamp (can be substituted with small zipties)
- GT2 7mm-wide, 20-tooth aluminium pulley same bore size as motor shaft
- 4x 30mm M3-0.5 cap-head machine screws
Tools
- Computer with Arduino IDE (I use Win7, Arduino 1.0.5 r2)
- Soldering iron with small chisel tip, solder, etc
- 2.5mm allen key for M5 screws
- 2mm allen key for M3 screws
- 1.5mm allen key for set screws in GT2 pulley
- multimeter for troubleshooting and current adjustment
- Narrow pliers for tightening in small spaces
Functional Overview
I'll cover adding the motor and pulleys to the slider, stringing the belt around and fastening it all up. It's a simple modification.
Then I'll cover how to put together a Pololu A4988 Black Edition kit and how to wire it up on a breadboard along with all the other external boards, as well as a simple plywood enclosure I knocked together in a few minutes for my 12V power supply (listed above) in order to prevent shocks as the wiring terminals are exposed.



The menu allows input of distance to travel, time to travel, number of steps to travel in and direction of travel. At the end of each step, the slider pauses while the camera is triggered.
Modifying the Slider
Step 1: Motor Assembly
The OpenBuilds V-Slot Actuator End Mount has NEMA 17 dimension holes in it, so four 30mm M3 cap head screws are all that's required to mount the motor to it.



Ensure the 20-tooth GT2 pulley is inside the mount before you slot the motor shaft in, as the mount isn't wide enough to add it on afterward. Once the motor is screwed on at the bottom, tighten up the set screws with one of them against the flat part of the motor shaft, ensuring that the teeth are directly in line with the centre of the whole extrusion unit.
Step 2: Idler Pulley Kit
The idler pulley kit goes together just like a wheel kit, and slots into the opposite end Actuator Mount:



Step 3: Belting Up
Feed the belt through the centre of the V-slot in line with the pulleys, ensuring the teeth are facing up.
Then feed it up and over the two pulleys and bring it back into the middle to the dolly build plate.



Here you wrap one side through the belt slot and clamp, or zip-tie it, then use that to tighten the entire belt through the whole system before connecting the other side. Not too tight for the motor to turn, but not loose enough to skip teeth on the drive pulley!
Assembling the Electronics
Step 1: Assemble the Stepper Driver
The Pololu A4988 Black Edition Stepper Motor Driver (technically the A4988 carrier board- the A4988 is the chip itself) typically comes in kit form, which simply means that the headers have to be soldered on. Since this is a power component, even though it is not driving the unit at its maximum capacity, it's a good idea to add a heatsink to help enhance its lifespan.
Break the header row in half to have two rows of eight. Slot these into the plated through holes in the board, and then slot this carefully into the breadboard. Solder the pins in place while the breadboard holds everything nice and perpendicular.



Once this is complete, cut off the corner of a small self-stick heatsink using a hacksaw or scrollsaw (carefully, in a clamp!) to mount to the A4988 IC.



Step 2: Breadboard-Mount the Components
Now everything needs to be mounted to breadboards so that it can be wired together into a functioning cicuit. I'm using separate boards for each part for the sake of clarity in images, but feel free to fit it all into a single board if you wish.
The LCD keypad shield cannot be mounted to a board, thanks to Arduino's strange choice to adhere to a design flaw rather than meet standards. This will be kept separate, though screwing it to a piece of wood or something to protect the pins might not be a bad idea.



The camera trigger circuit at its most simple consists of a resistor, a transistor and a 2.5mm TRS sub-mini plug. I've added a LED that will blink when the trigger pin spikes high, and a 3.5mm TRS mini jack in order to allow flexibility.
If you're buying components for this build, a 3.5mm socket designed for 0.1" pitch boards would be a good idea, but mine is from the scavenged pile so I've soldered a connector to it instead.
Lay everything out, ready to wire it all up.
Step 3: Wiring Everything Together
Time to grab all of the jumper cables. Having enough to keep things colour coded will make life easier when troubleshooting. Refer to the circuit diagram at the top if the following description confuses you at any point.



First wire up the LCD. Grab 10 female jumpers and connect them to the following shield pins: digital pins 4-9, power bus pins reset (if you want to use the LCD reset button), 5V & one of the GNDs.
If you have female-to-male jumpers, you can leave it there. Otherwise, connect male jumpers—to the other end of the females—in order to plug them into the corresponding Arduino header sockets. If you have an LCD keypad shield which has passthrough female headers installed on top, you can skip this step since your shield isn't blocking anything.
Next, the Pololu A4988 board. This needs eight jumpers on one side, I've used black and red for logic/motor power at easch end, and red/green/blue/yellow in the centre four to match up with the stepper motor's servo leads.
The logic power pin goes to 3.3V on the arduino, as the LCD above is using the 5V pin. The motor power leads go to your 12V power supply. On the other side, near the A4988 chip, I'm using blue and orange for STP and DIR respectively to contrast with the relatively uniform colours everywhere else. They go to Arduino pins 11 and 12 respectively, unless you modify the code. Then short RST and SLP together to keep the board enabled; I've used the white lead here.



Finally wire up the camera trigger switch circuit. Here the black wires are ground- the A row wire to Arduino, the C row wire to the 3.5mm socket. The yellow goes to Arduino pin 13 (so there's an LED indicator on the board as well as at the switch!), and the red wire goes to the other side of the 3.5mm socket (or 2.5mm plug lead if you're going that route).
Plug the stepper motor into the coloured wires according to the A4988 board diagram and your stepper's datasheet. For me, that was like this:



Caution: remember that the wires providing power to the motor will probably be pulling 1-2A at your chosen voltage, so ensure the wires used are rated for it. The A4988 chip and board around it may get hot! The potentiometer built into the board provides current limitation to protect both the A4988 and the motor, so ensure you set it appropriately before use using a multimeter.
Setting Up The Program
Once the components have been assembled, you can move to the coding. Download the zip included with this tutorial, or check this GitHub respository if you prefer. I’ll describe how I put it together so you can understand the general program flow and how the modules work together.
Step 1: Includes and Basic Definitions
The only include necessary for this was the LCD writing library LiquidCrystal.h
. This gives access to the lcd.xxxx()
functions. There's a pow()
in the program, and I found that including the C++ library math.h
isn't necessary as a few of its most helpful functions are included in the stock Arduino environment, including pow()
.
1 |
#include <LiquidCrystal.h> |
2 |
|
3 |
LiquidCrystal lcd(8, 9, 4, 5, 6, 7); //set LCD output pins |
4 |
|
5 |
//define stepper driver pins
|
6 |
const int stp = 11; //can't use pin 10 with the SS LCD as it's the backlight control. |
7 |
//if it goes low, backlight turns off!
|
8 |
const int dir = 12; |
9 |
|
10 |
//define trigger pin
|
11 |
const int trig = 13; |
12 |
|
13 |
//BUTTONS
|
14 |
//define button values
|
15 |
const int btnUp = 0; |
16 |
const int btnDn = 1; |
17 |
const int btnL = 2; |
18 |
const int btnR = 3; |
19 |
const int btnSel = 4; |
20 |
const int btnNone = 5; |
21 |
|
22 |
//define button-reading variables
|
23 |
int btnVal = 5; |
24 |
int adcIn = 0; |
I set the LCD output pins, the stepper driver output pins, and the camera trigger output pin. Once the actual hardware interface is set up, I added variables for button events followed by the button-reading function, which I adapted from the DFRobot wiki on their identical LCD keypad shield. Note that SainSmart provides no documentation.
Step 2: Setup() Loop
This is super straightfoward. Initialise the LCD and relevant output pins, followed by a basic welcome screen, then drop into the home screen: menu option 1 with values zeroed.
1 |
void setup() { |
2 |
lcd.begin(16, 2); // initialise LCD lib full-screen |
3 |
lcd.setCursor(0,0); // set cursor position |
4 |
|
5 |
pinMode(stp, OUTPUT); //initialise stepper pins |
6 |
pinMode(dir, OUTPUT); |
7 |
|
8 |
pinMode(trig, OUTPUT); //initialise trigger pin |
9 |
digitalWrite(trig, LOW); //ensure trigger is turned off |
10 |
|
11 |
lcd.print("Welcome to"); //welcome screen |
12 |
lcd.setCursor(0,1); |
13 |
lcd.print("SliderCam v0.2!"); |
14 |
delay(1000); |
15 |
lcd.clear(); |
16 |
lcd.print(menuItemsTop[0]); |
17 |
delay(100); |
18 |
lcd.setCursor(0,1); |
19 |
for (int i = 0; i < 4; i++) { |
20 |
lcd.setCursor(i, 1); |
21 |
lcd.print(currentDistance[i]); |
22 |
}
|
23 |
lcd.setCursor(4,1); |
24 |
lcd.print("mm(max 1300)"); |
25 |
}
|
Step 3: Monitor Buttons
The advantage here coding-wise is that the equipment doesn't need to do anything at all without user input. Which means that the very first thing can simply be an eternal button poll loop. Calling the readLcdButtons()
function over and over until its value changes doesn't negatively impact program performance, and you don't need to worry about leaving interrupt pins available.
1 |
void loop() { |
2 |
do { |
3 |
btnVal = readLcdButtons(); //continually read the buttons... |
4 |
}
|
5 |
while (btnVal==5); //...until something is pressed |
1 |
//declare button poll function
|
2 |
int readLcdButtons() { |
3 |
delay(90); //debounce delay, tuned experimentally. delay is fine as program shouldn't be doing anything else |
4 |
//at this point anyway
|
5 |
adcIn = analogRead(0); //read value from pin A0 |
6 |
|
7 |
/*threshold values confirmed by experimentation with button calibration sketch returning the following ADC read values:
|
8 |
right: 0
|
9 |
up: 143
|
10 |
down: 328
|
11 |
left: 504
|
12 |
select: 741
|
13 |
*/
|
14 |
|
15 |
if (adcIn > 1000) return btnNone; |
16 |
if (adcIn < 50) return btnR; |
17 |
if (adcIn < 250) return btnUp; |
18 |
if (adcIn < 450) return btnDn; |
19 |
if (adcIn < 650) return btnL; |
20 |
if (adcIn < 850) return btnSel; |
21 |
|
22 |
return btnNone; //if it can't detect anything, return no button pressed |
23 |
}
|
ReadLcdButtons()
has a delay of 90ms in order to debounce the buttons. In actuality, this isn't a debounce, as it doesn't re-take the ADC measurement after a set amount of time, but rather polls the buttons infrequently enough to rarely register more than a single click.
It achieves the same thing from a practical UX view. It's more of a poll buttons every 90ms rather than constantly, which is why use of delay()
is generally not considered good practice for debouncing purposes, but it fixed the problem (only each end of menus were accessible).
Step 4: Screen Refresh
Once the unit can react to input, there needs to be a way for it to display those reactions.
After trying on-the-fly updates, I determined that consistent screen refresh like a real OS was easier to manage in my attempts at a modular upgradeable structure. Doing this is as simple as clearing the screen, then rebuilding based on known current parameters.
This sounds complicated, but in practice makes life much easier. It removes a large number of LCD commands from elsewhere in the program, and creates a variable-type-agnostic zone which is minimally affected by program updates external to it.
The actual refreshing part evolved to consist of four distinct steps:
Reset parameters...
1 |
//PRINT NEW SCREEN VALUES
|
2 |
btnVal=btnNone; |
3 |
lcd.clear(); |
...print the top line...
1 |
lcd.setCursor(0, 0); |
2 |
lcd.print(menuItemsTop[currentMenuItem]); //print top level menu item |
...print the bottom line, which I'll explain a little later...
1 |
lcd.setCursor(0,1); |
2 |
switch (currentMenuItem) { |
3 |
case 0: |
4 |
{
|
5 |
for (int i = 0; i < 4; i++) { |
6 |
lcd.setCursor(i, 1); |
7 |
lcd.print(currentDistance[i]); |
8 |
}
|
9 |
break; |
10 |
}
|
11 |
|
12 |
case 1: |
13 |
{
|
14 |
for (int i = 0; i < 6; i++) { |
15 |
lcd.setCursor(i, 1); |
16 |
lcd.print(currentDuration[i]); |
17 |
}
|
18 |
break; |
19 |
}
|
20 |
|
21 |
case 2: |
22 |
{
|
23 |
for (int i = 0; i < 4; i++) { |
24 |
lcd.setCursor(i, 1); |
25 |
lcd.print(currentSteps[i]); |
26 |
}
|
27 |
break; |
28 |
}
|
29 |
|
30 |
case 3: |
31 |
{
|
32 |
if (travelDir == 0) lcd.print("From Motor"); |
33 |
else lcd.print("To Motor"); |
34 |
break; |
35 |
}
|
36 |
|
37 |
case 4: |
38 |
{
|
39 |
lcd.print("Stop!"); |
40 |
break; |
41 |
}
|
42 |
} //end switch |
...and add screen-specific commands over the top of what's already printed.
1 |
if (currentMenuItem==0){ |
2 |
lcd.setCursor(4,1); |
3 |
lcd.print("mm(max 1300)"); //insert max carriage travel on slider used |
4 |
} |
5 |
if (currentMenuItem==1){ |
6 |
lcd.setCursor(6,1); |
7 |
lcd.print("s(3600/hr)"); |
8 |
} |
9 |
if (currentMenuLevel == 1) { |
10 |
lcd.setCursor(currentCursorPos, 1); |
11 |
lcd.blink(); |
12 |
} |
13 |
else lcd.noBlink(); |
Building A Menu: Main Headings
Naturally, that exact screen refresh section doesn't write itself, and we need to know the menu it's writing to screen before it can be completed. The main headings are easy, since they don't actually change depending on user input. This means it can simply be a string array- technically a char pointer array, or an array of arrays:
1 |
//MENU GUI
|
2 |
//define top-level menu item strings for numerical navigation
|
3 |
char* menuItemsTop[] = { |
4 |
" 01 Distance >", "< 02 Duration >", "< 03 Steps > ", "< 04 Direction >", "< 05 Go!"}; |
5 |
|
6 |
int currentMenuLevel = 0; //top menu or submenu |
7 |
int currentMenuItem = 0; //x-axis position of menu selection |
8 |
int currentCursorPos = 0; //current lcd cursor position |
9 |
int currentDistance[4] = { |
10 |
0, 0, 0, 0}; |
11 |
int currentDuration[6] = { |
12 |
0, 0, 0, 0, 0, 0}; |
13 |
int currentSteps[4] = { |
14 |
0, 0, 0, 1}; |
This means that this menuItemsTop
array can be navigated simply by altering the number inside the square brackets at screen refresh time. Which just so happens, since everything is zero-indexed, to track identically with the integer currentMenuItem
.
Manipulating currentMenuItem
on button events allows us one-dimensional navigation, so when you see menuItemsTop[currentMenuItem]
it's obviously current menu heading.
1 |
if (currentMenuLevel==0) { |
2 |
switch (btnVal){ |
3 |
case btnL: |
4 |
{
|
5 |
if (currentMenuItem == 0) break; //can't go left from here |
6 |
else currentMenuItem--; |
7 |
break; |
8 |
}
|
9 |
|
10 |
case btnR: |
11 |
{
|
12 |
if (currentMenuItem == 4) break; //can't go right from here |
13 |
else currentMenuItem++; |
14 |
break; |
15 |
}
|
16 |
|
17 |
case btnSel: |
18 |
{
|
19 |
currentMenuLevel++; |
20 |
if (currentCursorPos > 3 && (currentMenuItem == 0 || currentMenuItem == 2)) currentCursorPos = 3; //don't go off the end of the numbers for the 4-digit numbers |
21 |
if (currentCursorPos > 0 && (currentMenuItem > 2)) currentCursorPos = 0; // set blinking cursor to left for text-based options |
22 |
if (currentMenuItem == 4) { |
23 |
motion = 1; |
24 |
motionControl(); |
25 |
break; |
26 |
}
|
27 |
}
|
28 |
} //end of switch |
29 |
} //end of level 0 |
So you can move left and right, and go into a menu, or in the case of Go! then motion control is activated. Which is all that's required here.
Building A Menu: Submenu
The submenu system took a little more doing, thanks to its internal complexity. The first three entries, Distance, Duration and Steps, technically consist of a sub-sub-menu, each allowing navigation of the multi-digit value as well as each individual character.
This is covered by making each submenu entry a switched heading system in its own right. While this was a long way around, it's a simple and consistent method of allowing such low-level navigation. Since I really just figured out the Distance submenu and then copied it over for the other submenus, here's a look at that one.
1 |
else { // i.e. "else if currentMenuLevel = 1" |
2 |
if (currentMenuItem == 0) { //01 DISTANCE |
3 |
|
4 |
switch (btnVal) { |
5 |
case btnUp: |
6 |
{
|
7 |
currentChar = currentDistance[currentCursorPos]; |
8 |
adjustDigit(currentChar, 1); |
9 |
currentDistance[currentCursorPos] = currentChar; |
10 |
break; |
11 |
}
|
12 |
|
13 |
case btnDn: |
14 |
{
|
15 |
currentChar = currentDistance[currentCursorPos]; |
16 |
adjustDigit(currentChar, 0); |
17 |
currentDistance[currentCursorPos] = currentChar; |
18 |
break; |
19 |
}
|
20 |
|
21 |
case btnL: |
22 |
{
|
23 |
if (currentCursorPos == 0) break; //can't go left from here |
24 |
else currentCursorPos--; |
25 |
break; |
26 |
}
|
27 |
|
28 |
case btnR: |
29 |
{
|
30 |
if (currentCursorPos == 3) break; //can't go left from here |
31 |
else currentCursorPos++; |
32 |
break; |
33 |
}
|
34 |
|
35 |
case btnSel: |
36 |
{
|
37 |
parseArrayDistance(); |
38 |
currentMenuLevel--; |
39 |
}
|
40 |
} //end switch |
41 |
} //end DISTANCE |
Left and right are essentially the same as the top-level menu, simply moving back and forth along the number in the same way, by having the number actually be a set of digits in an int array and the current location stored in an int called currentCursorPos
which allows blinking as seen in the screen refresh module above.
Printing these arrays along the bottom LCD row is what the for loops were for in the screen refresh section; i
from 0 to 3, LCD column
from 0 to 3, currentDistance[]
from 0 to 3.
1 |
int adjustDigit(int x, int dir){ //digit adjust function |
2 |
if (dir == 0 && x > 0) x--; //subtract from digit on btnDn |
3 |
if (dir == 1 && x < 9) x++; // add to digit on btnUp |
4 |
lcd.setCursor(currentCursorPos, 1); |
5 |
lcd.print(x); |
6 |
currentChar = x; |
7 |
return currentChar; //return new digit |
8 |
}
|
Increasing and decreasing the number is achieved by storing the current digit in the variable currentChar
, which is then passed to the adjustDigit()
function along with a boolean value indicating direction; to increase or decrease the motor.
This simply adjusts the digit according to the boolean value and saves the result, whereon flow returns to the main loop, where the currentChar value is saved back into the correct position of the original currentDistance[]
array and the new adjusted digit is printed at screen refresh.
Parsing Display Array Values
When Select is hit from one of the number array submenus, it triggers the relevant parsing function- in this case parseArrayDistance()
. You need to parse the array, used to conveniently display and edit, into an integer useful for actual motion calculations. I chose to do this now rather than on Go! to keep UX feeling snappy.
1 |
int adjustDigit(int x, int dir){ //digit adjust function |
2 |
if (dir == 0 && x > 0) x--; //subtract from digit on btnDn |
3 |
if (dir == 1 && x < 9) x++; // add to digit on btnUp |
4 |
lcd.setCursor(currentCursorPos, 1); |
5 |
lcd.print(x); |
6 |
currentChar = x; |
7 |
return currentChar; //return new digit |
8 |
}
|
I came up with this function from the one useful passing comment I found after exhausting Google looking for standard array-to-int functions, coming up empty, and getting rid of the mess of array-to-char-to-int functions that were an ineffective workaround. It seems fairly short and light given that it's literally based on the foundation of decimal mathematics, but if you know a better method, I'm all ears.
Motion Control and Camera Triggering
All the values are set and you hit Go! What happens next? You need to calculate exactly what the numbers given to it are supposed to do in order to run the final motion. This part is functional, but a work in progress; I feel there need to be more options for different types of motion.
1 |
int motionControl() { |
2 |
totalMotorSteps = currentDistanceInt * 5; //calculate total steps (0.2mm = 20-tooth gear on 2mm pitch belt; 40mm per rev, 200 steps per rev, ergo 1/5th mm per step) |
3 |
pulseDelay = (1000L * (currentDurationInt - (currentStepsInt * shutterDuration))) / totalMotorSteps; //how long to pause in ms between STP pulses to the motor driver |
4 |
intervalDistance = totalMotorSteps / currentStepsInt; |
What's happening in this function is fairly clear from the commenting, I think. I've put a shutterDuration
of 2 seconds in the software, mainly to keep testing fairly rapid. If you're shooting at night, at lower ISOs, this might need to be more like 25-35 seconds depending on your exact shutter speed.
The pulseDelay
is multiplied by 1000
at the end to convert from seconds to milliseconds, of course. The L
to convert the constant int to a long is more me erring on the side of caution than actually being strictly necessary. Since it's a relatively small sketch, I'm not overly concerned about variable memory usage.
These calculations assume that the loop itself requires a negligible amount of time to run in comparison to the pulseDelay
time, which, once I took out the button polling, appears to be true.
1 |
//once per overall run
|
2 |
if (travelDir == 0) digitalWrite(dir, LOW); |
3 |
else if (travelDir == 1) digitalWrite(dir, HIGH); |
4 |
//Serial.begin(9600);
|
5 |
//Serial.println(pulseDelay);
|
6 |
|
7 |
//step loop
|
8 |
do { |
9 |
digitalWrite(stp, HIGH); //fire motor driver step |
10 |
delay(pulseDelay); |
11 |
digitalWrite(stp, LOW); //reset driver |
12 |
//btnVal = readLcdButtons(); //check there's no stoppage - this takes too long and significantly slows motor; use reset for stop!
|
13 |
currentStep++; |
14 |
|
15 |
//at end of each step
|
16 |
if (currentStep % intervalDistance == 0) { //if current number of motor steps is divisible by the number of motor steps in a camera step, fire the camera |
17 |
digitalWrite(trig, HIGH); //trigger camera shutter |
18 |
delay(80); |
19 |
digitalWrite(trig, LOW); //reset trigger pin |
20 |
delay((shutterDuration * 1000)-80); //delay needs changing to timer so stop button can be polled |
21 |
}
|
22 |
|
23 |
}
|
24 |
while (currentStep < totalMotorSteps); |
25 |
|
26 |
} //end motion control |
Lastly, note the currentSteps
value set at 1. I haven't created an error-checking function for this, but simple common sense says stepSize
becomes infinite if currentStepsInt == 0
, so it's best to keep it at one if continuous motion is desired. I added an improvement entry for this already.
Running the Final Product
For something running on code written more or less from scratch in two days and bugfixed over two more, it works like a dream! The proof is in the pudding, though. Does it actually get worthwhile timelapse footage, and does the control unit really work well in the field?
In my tests, the answer appears to be a whole-hearted yes. Below is a two-hour, 650-frame timelapse, the very first test. The slider also completed a 9 hour 720 frame test flawlessly, but unfortunately the camera battery didn't do so well after 2 hours in ...which I didn't find out until the 8.5 hour mark, naturally.
If I set the time and steps appropriately, the motion can be continuous for slow dolly moves in live action video, though the jerky ends need editing out or speed-ramping.
Sound may be an issue unless
your stepper is very quiet, but for adding production value to
self-recordings, it's an option.
Improvements
As
with anything, there are possible improvements to be made. I've listed
these at the top of the .ino
file, though admittedly without particular
care over feasibility nor ordered by any kind of importance.
Some of
these I considered fixing prior to releasing this tutorial with v0.2,
but I feel like they in themselves are a learning experience to look at
in terms of mentally dismantling the usability of a program.
1 |
IMPROVEMENTS AND CONSIDERATIONS TOWARDS V1.0: |
2 |
1) Efficiency of submenu button response code for first three menu headers |
3 |
2) Use of bulb shutter time as an extra menu option, passed to shutterDuration int |
4 |
3) shutter duration should be timed pause, not delay() - can't poll stop button! |
5 |
4) Use EEPROM library functions to save quantities, can thus simplify the motion control section and use Reset as "stop" |
6 |
5) Remove switch from "Go" submenu, replace with more appropriate logic statement |
7 |
6) Would it be better to time camera steps rather than total travel? "duration" being more like 15 sec or 2 min than 30 min or 4hrs? |
8 |
7) Any const ints that would be better as #define or ints better as boolean? Hardly running against the limits of SRAM space at 8kB, though. |
9 |
8) Tweening/easing for acceleration curves, particularly for video use |
10 |
9) Error check for zero step size, or simply add one to intervalDistance if value is zero before calculations- other end of Distance is still 1 step |
11 |
10) Would sub-16ms delay()s be better as delayMicroseconds()? How much do interrupts throw off timing? |
12 |
11) Use of sleep on A4988 to reduce power consumption in the field? |
13 |
12) Error check for currentDurationInt <= currentStepsInt*shutterDuration, allowing no time for movement or even negative pulseDelay! |
14 |
*/
|
These
are just the improvements I've thought of so far, in an effort to guide
the codebase from a rudimentary but functional v0.2 towards a more
optimised and more capable release v1.0. You may notice more. Feel
free to leave them in the comments below or on GitHub.
Wrapping Up
If you've followed from beginning to end, including the Photography Tuts+ portion of the build, you're now the proud owner of a high quality motorised camera slider that can produce timelapse footage and subtle dolly moves. If you're using the code for some other project, I'd love to see it.
In this tutorial, I've looked at different forms of loop-based flow control, creating a rudimentary GUI and updating an LCD based on user input. I also looked at simultaneously controlling multiple external mechanical devices via breakout boards.
You've seen
the flow and ease of programming modular code, as well as seeing ideas
for how to improve code that is functional, but not optimised, both from
UX and processor efficiency standpoints. These tools should serve you
well for a variety of communication- and interaction-based projects in
future.
Please leave any questions or comments the comments below!