Arduino-based DIY camera slider
This is the first iteration of my interpretation of a DIY camera slider. I wanted to make something useful using the experience I gained using stepper motors. A camera slider is a nice equipment for videos, timelapses…etc., so I though I should build my own version. I wanted to make it relatively simple, so most of the parts can be sourced from web shops or can be printed in a short amount of time. As I mentioned, this is the first iteration, so future versions will be released where I will improve/upgrade the mechanism, the software, or the electronics. The final goal is to make a robust, reliable and relatively cheap slider with a lot of useful features.
Wiring Schematics
Arduino source code
#include <AccelStepper.h> //accelstepper library AccelStepper stepper(1, 8, 9); // DIR:9, STEP: 8 #include <Wire.h> //i2C #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels#define OLED_RESET #define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin) #define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); //Notice: Maybe a more "lightweight" library should be used to reduce the amount of needed resources const byte Analog_X_pin = A0; //x-axis readings int Analog_X = 0; //x-axis value int Analog_X_AVG = 0; //x-axis value average int AVG_Diff = 0; //difference between the initialized and current value //--------------------------------------------------------------------------------- const byte RotaryCLK = 2; //CLK pin on the rotary encoder (must be an interrupt pin) const byte RotaryDT = 4; //DT pin on the rotary encoder const byte RotarySW = 5; //SW pin on the rotary encoder (Button function) const int LimitSwitch_1 = 10; //Input for the limit switch const int LimitSwitch_2 = 11; //Input for the limit switch //const int stepperEnablePin = 7; //Enable for the stepper motor control circuit - if needed //Statuses of the DT and CLK pins on the encoder int CLKNow; int CLKPrevious; int DTNow; int DTPrevious; int RotaryButtonValue; //Pressed or not pressed rotary switch volatile int menuCounter = 0; //this is for counting the main menu (menu item number) float RotaryButtonTime = 0; //timer for the rotary encoder's button float previousInterrupt = 0; //timer for the limit switch volatile float travelDistance = 10; // cm float travelSteps; //steps volatile float travelSpeed = 10; // mm/s float travelVelocity; //steps/s volatile float travelTime = 0.17; // minutes float microStepping = 3200; //make sure you set it up correctly on the stepper motor controller too! bool stepperHoming_Selected = false; bool stepperHoming_Completed = false; int homingPosition = 1; //1: motor side, 2: tensioner side bool joystickMovement_Selected = false; bool stepperSpeed_Selected = false; bool stepperDistance_Selected = false; bool stepperTime_Selected = false; bool startSlider_Selected = false; bool valueChanged; bool menuChanged; bool updateValueSelection; float limitSwitchTempPosition; //position where the limit switch was hit //DRV8825 microstepping values /* MS M0 M1 M2 200 L L L 400 H L L 800 L H L 1600 H H L 3200 L L H -> I use this 6400 H H H */ void setup() { //SERIAL Serial.begin(9600); Wire.begin(); Wire.setClock(800000L); //---------------------------------------------------------------------------- if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); 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(); delay(1000); // Pause for 1 second // Clear the buffer (contents of the display) display.clearDisplay(); //---------------------------------------------------------------------------- //PINS pinMode(Analog_X_pin, INPUT); //A0 pinMode(RotaryCLK, INPUT); //CLK pinMode(RotaryDT, INPUT); //DT pinMode(RotarySW, INPUT_PULLUP); //SW //pinMode(stepperEnablePin, OUTPUT); //Enable pin for the stepper motor control circuit -- if needed pinMode(LimitSwitch_1, INPUT_PULLUP); //LimitSwitch_1 (default is 1 for me) pinMode(LimitSwitch_2, INPUT_PULLUP); //LimitSwitch_2 (default is 1 for me) //---------------------------------------------------------------------------- InitialValues(); //averaging the values of the analog pin (value from potmeter) //Store states CLKPrevious = digitalRead(RotaryCLK); DTPrevious = digitalRead(RotaryDT); //Rotary encoder interrupts attachInterrupt(digitalPinToInterrupt(RotaryCLK), RotaryEncoder, CHANGE); //---------------------------------------------------------------------------- //Stepper parameters //setting up some default values for maximum speed and maximum acceleration stepper.setMaxSpeed(5000); //SPEED = Steps / second --- absolute maximum speed, actually, the clock frequency limits it. stepper.setAcceleration(1000); //ACCELERATION = Steps /(second)^2 --- we do not use accelerations, so it is kind of irrelevant delay(200); //digitalWrite(stepperEnablePin, HIGH); //Enable the stepper motor control circuit -- if needed //Load the menu updateMenuPosition(); } void loop() { ReadAnalog(); if (startSlider_Selected == true) { stepper.runSpeedToPosition(); //Move without acceleration - depends on move() and setSpeed() //why is this in an if()? --- It conflicts with the joytick's runSpeed() otherwise } CheckRotaryButton(); if (menuChanged == true) { updateMenuPosition(); } if (updateValueSelection == true) { updateSelection(); } if (valueChanged == true ) { updateValue(); } LimitSwitchPressed(); //checking the limit switch in every iteration } void ReadAnalog() { if (joystickMovement_Selected == true) { //Reading the X potentiometer in the joystick Analog_X = analogRead(Analog_X_pin); AVG_Diff = Analog_X - Analog_X_AVG; //calculating the deviation from the average (initialized value) //if the value is 25 "value away" from the average (midpoint), we allow the update of the speed //This is a sort of a filter for the inaccuracy of the reading and the position after releasing the stick if (abs(AVG_Diff) > 25) { stepper.setSpeed(3 * (AVG_Diff)); //the x3 multiplier is an arbitrary/empirical value. stepper.runSpeed(); //step the motor (this will step the motor by 1 step at each loop indefinitely (as long as speed != 0)) } else { stepper.setSpeed(0); } //valueChanged = true; //This would show the value of the joystick, but the Nano is too slow for this rapid display update } else { //Do nothing } } void InitialValues() { //Set the values to zero before averaging float tempX = 0; //---------------------------------------------------------------------------- //read the analog 50x, then calculate an average. for (int i = 0; i < 50; i++) { tempX += analogRead(Analog_X_pin); delay(10); //allowing a little time between two readings } //---------------------------------------------------------------------------- Analog_X_AVG = tempX / 50; //---------------------------------------------------------------------------- Serial.print("AVG_X: "); Serial.println(Analog_X_AVG); Serial.println("Calibration finished!"); } void RotaryEncoder() { if (stepperHoming_Selected == true) //homing to which side { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { if (homingPosition < 2) { homingPosition++; } else { //Do nothing } } else { if (homingPosition == 1) { //Do nothing } else { homingPosition--; } } valueChanged = true; } CLKPrevious = CLKNow; // Store last CLK state } else if (joystickMovement_Selected == true) { //do nothing } else if (stepperDistance_Selected == true) //set distance in millimeters { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { travelDistance++; } else { travelDistance--; } recalculateTime(); //when the distance is changed, the speed is kept constant and the time is recalculated valueChanged = true; } CLKPrevious = CLKNow; // Store last CLK state } //Set distance else if (stepperSpeed_Selected == true) { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { travelSpeed++; } else { travelSpeed--; } recalculateTime(); //when the speed is changed, the distance is kept constant and the time is recalculated valueChanged = true; } CLKPrevious = CLKNow; // Store last CLK state } //Set time else if (stepperTime_Selected == true) { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { travelTime = travelTime + 0.1; } else { travelTime = travelTime - 0.1; } recalculateSpeed(); //When the time is changed, the distance is kept constant and the speed is recalculated valueChanged = true; } CLKPrevious = CLKNow; // Store last CLK state } else if (startSlider_Selected == true) { //do nothing } else //MENU COUNTER---------------------------------------------------------------------------- { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating CCW so increase if (digitalRead(RotaryDT) != CLKNow) { if (menuCounter < 5) //5 menu items 0-5 { menuCounter++; } else { menuCounter = 0; //0 comes after 5, so we move in a "ring" } menuChanged = true; } else { // Encoder is rotating CW so decrease if (menuCounter > 0) { menuCounter--; } else { menuCounter = 5; //5 comes after 0 when we decrease the numbers } menuChanged = true; } } CLKPrevious = CLKNow; // Store last CLK state } } void updateMenuPosition() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); switch (menuCounter) //this checks the value of the counter { //Homing case 0: display.setCursor(10, 0); display.println(F("1-Homing")); display.setCursor(0, 20); display.setTextSize(3); display.println(F("1")); // break; //-------------------------------------------------------------- //Joystick case 1: display.setCursor(10, 0); display.println(F("2-Joystick")); display.setCursor(0, 20); display.setTextSize(3); display.println(F(" ")); break; //-------------------------------------------------------------- case 2: display.setCursor(10, 0); display.println(F("3-Distance (cm)")); display.setCursor(0, 20); display.setTextSize(3); display.println(travelDistance, 0); //can be negative too! break; //-------------------------------------------------------------- case 3: display.setCursor(10, 0); display.println(F("4-Speed (mm/s)")); display.setCursor(0, 20); display.setTextSize(3); display.println(travelSpeed, 1); //can be negative too! break; //-------------------------------------------------------------- case 4: display.setCursor(10, 0); display.println(F("5-Time (min)")); display.setCursor(0, 20); display.setTextSize(3); display.println(abs(travelTime), 1); //abs, because if negative distance is used, the time can become negative break; //-------------------------------------------------------------- case 5: display.setCursor(10, 0); display.println(F("6-Start slider")); display.setCursor(0, 20); display.setTextSize(2); display.println(F("Press the")); //print the instructions display.println(F("button!")); break; } display.display(); // Show text menuChanged = false; //next loop() iteration will not enter this part again } void CheckRotaryButton() { RotaryButtonValue = digitalRead(RotarySW); //read the button state if (RotaryButtonValue == 0) //0 and 1 can differ based on the wiring { if (millis() - RotaryButtonTime > 1000) { switch (menuCounter) { case 0: stepperHoming_Selected = !stepperHoming_Selected; //we change the status of the variable to the opposite if (stepperHoming_Selected == false) //if the variable became false -> we exited and also approved a position { stepperHoming(); } //Comment: homing cannot be stopped at the moment. This will be fixed in the next iteration break; // case 1: joystickMovement_Selected = !joystickMovement_Selected; break; // case 2: stepperDistance_Selected = !stepperDistance_Selected; break; // case 3: stepperSpeed_Selected = !stepperSpeed_Selected; break; // case 4: stepperTime_Selected = !stepperTime_Selected; break; case 5: startSlider_Selected = !startSlider_Selected; if (startSlider_Selected == false) //If we exit this part, the motor stops { if (stepper.distanceToGo() != 0) //If the stepper is currently moving { stepper.stop(); //"soft" stop - decelerates to 0. } //Update display display.setTextSize(2); display.setCursor(0, 20); display.setTextColor(WHITE, BLACK); display.println(F(" ")); display.setCursor(0, 20); display.setTextSize(2); display.println(F("Stopped!")); display.display(); } break; } RotaryButtonTime = millis(); updateValueSelection = true; } } } void updateSelection() { display.setTextSize(1); display.setTextColor(SSD1306_WHITE); if (stepperHoming_Selected == true) { display.setCursor(0, 0); display.println(F(">")); } else if (joystickMovement_Selected == true) { display.setCursor(0, 0); display.println(F(">")); } else if (stepperDistance_Selected == true) { display.setCursor(0, 0); display.println(F(">")); } else if (stepperSpeed_Selected == true) { display.setCursor(0, 0); display.println(F(">")); } else if (stepperTime_Selected == true) { display.setCursor(0, 0); display.println(F(">")); } else if (startSlider_Selected == true) { display.setCursor(0, 0); display.println(F(">")); display.setTextSize(2); display.setCursor(0, 20); display.setTextColor(WHITE, BLACK); display.println(F(" ")); display.display(); display.setTextColor(WHITE, BLACK); display.println(F(" ")); display.display(); display.setCursor(0, 20); display.setTextSize(2); display.println(F("Started!")); recalculateSpeed(); convertValues(); stepper.move(travelSteps); stepper.setSpeed(travelVelocity); //It is important to first set the steps (move()), then set the speed (setSpeed()). } else { //Clears the > symbol from the display which indicates that we are not in the submenu anymore display.setCursor(0, 0); display.fillRect(0, 0, 10, 10, SSD1306_BLACK); } display.display(); updateValueSelection = false; //next loop() iteration will not enter } void updateValue() { display.setTextSize(3); display.setTextColor(WHITE, BLACK); display.setCursor(0, 20); display.println(F(" ")); display.setCursor(0, 20); switch (menuCounter) //this checks the value of the counter (0, 1, 2 or 3) { //Homing case 0: display.println(homingPosition); break; //------------------------------- //Joystick case 1: display.println(3 * (AVG_Diff)); //shows the position of the joystick * 3 ---> speed in steps/s //Currently, this is not in use, since it is very CPU intense and it breaks the motor's movement break; //------------------------------- //Distance case 2: display.println(travelDistance, 0); break; //------------------------------- //Speed case 3: display.println(travelSpeed, 1); break; //------------------------------- //Time case 4: display.println(abs(travelTime), 2); //time is abs because negative distance can result in negative time break; //------------------------------- } display.display(); // Show initial text valueChanged = false; //next loop() iteration will not enter again } void recalculateSpeed() { //If you change time, speed will be recalculated // v = s/t travelSpeed = (10 * travelDistance) / (60.0 * travelTime); //60x because the time is expressed in minutes //(10*[cm]) /(60*[min]) ---> (mm)/[s] //Serial.print("Recalculated speed: "); //Serial.println(travelSpeed); } void recalculateTime() { //If you change distance or speed, time will be recalculated // t = s/v travelTime = (10 * travelDistance / travelSpeed) / 60.0; //x10 because cm to mm, division by 60 is because time is in minutes //(10*[cm])/[mm/s]/60 ---> [mm]/[mm/s]/60 ---> [s]/60 ---> [min] } void convertValues() { //distance in cm to steps //Pulley's pitch diameter = 12.732 mm, 20 teeth, GT2 pulley. travelSteps = (10 * travelDistance) * (microStepping / (12.732 * 3.1415)); //microstepping/(diameter*pi) //10*[cm] * [steps/turn]/[mm] ---> [cm] * [steps/turn] //Serial.print("Steps to do: "); //Serial.println(travelSteps); travelVelocity = travelSpeed * (microStepping / (12.732 * 3.1415)); //microstepping/(diameter*pi), because mm/s to steps/s //Serial.print("Velocity (steps/s): "); //Serial.println(travelVelocity); } void stepperHoming() { if (homingPosition == 1) //Motor side home { //homing part - negative direction while (digitalRead(LimitSwitch_1) == 1) //0 or 1, depends on the wiring! { stepper.setSpeed(-600); //going towards the motor stepper.runSpeed(); } //parking part - positive direction while (digitalRead(LimitSwitch_1) == 0) //0 or 1, depends on the wiring! { stepper.setSpeed(200); //moving away from the motor until the switch is off stepper.runSpeed(); } stepper.setCurrentPosition(0); //reset the position to 0 //Notice that the above movements are enclosed in while() loops, therefore they cannot be interrupted in this version of the code } else //homingPosition == 2; // tensisoner side home { while (digitalRead(LimitSwitch_2) == 1) //0 or 1, depends on the wiring! { stepper.setSpeed(600); //going towards the tensioner stepper.runSpeed(); } //parking part - positive direction while (digitalRead(LimitSwitch_2) == 0) //0 or 1, depends on the wiring! { stepper.setSpeed(-200); //moving away from the tensioner until the switch is off stepper.runSpeed(); } //stepper.setCurrentPosition(0); //reset the position to 0 //comment: we might not want to reset to zero here. } //Update display - redraw everything display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(10, 0); display.println(F("1-Homing")); display.setCursor(0, 20); display.setTextSize(3); display.println(F("Parked!")); display.display(); } void LimitSwitchPressed() { //--- Limit switch 1 - motor side if (digitalRead(LimitSwitch_1) == 0) //0 or 1, depends on the wiring! { //Serial.println("LS-1"); //Checking and identifying the switch if (joystickMovement_Selected == true) { joystickMovement_Selected = false; //kill the joystick //This option takes away the joystick control from the use and automatically parks the gantry } if (millis() - previousInterrupt > 300) { stepper.stop(); //"soft" stop - decelerates to 0. previousInterrupt = millis(); //Alternatively we can make it to go to a specific place: //stepper.moveTo(0); //This goes back to absolute 0, which is technically the homing while (digitalRead(LimitSwitch_1) == 0) //0 or 1, depends on the wiring! { stepper.setSpeed(200); //moving away from the switch - moving towards the tensioner side stepper.runSpeed(); } stepper.setCurrentPosition(0); //we set the position to zero, because this side is always the origin with the value of zero } } //--- Limit switch 2 - tensioner side if (digitalRead(LimitSwitch_2) == 0) //0 or 1, depends on the wiring! { //Serial.println("LS-2"); //Checking and identifying the switch if (joystickMovement_Selected == true) { joystickMovement_Selected = false; //kill the joystick //This option takes away the joystick control from the use and automatically parks the gantry } if (millis() - previousInterrupt > 300) { limitSwitchTempPosition = stepper.currentPosition(); //we save the position where the limit switch was hit stepper.stop(); //"soft" stop - decelerates to 0. //stepper.runToPosition(); previousInterrupt = millis(); //Alternatively we can make it to go to a specific place: //stepper.moveTo(0); //This goes back to absolute 0, which is technically the homing while (digitalRead(LimitSwitch_2) == 0) //0 or 1, depends on the wiring! { stepper.setSpeed(-200); //moving away from the switch - moving towards the motor side stepper.runSpeed(); } stepper.setCurrentPosition(limitSwitchTempPosition); //we set the position where the limit switch was hit - absolute max number we can have, depends on the limit switch's physical position } } }
3d models for printing
There are two versions of legs because you will need both of them in order to have the mounting holes facing towards the inside of the profile. The legs use 5 mm holes. The motor mount has 3 mm holes for the motor and 5.5 mm holes for the mounting. The belt tensioner has three 5.5 mm holes. The middle hole holds the idler pulley.
Disclaimer: Despite the fact that the exact same parts were used during the demonstration I do not take the responsibility for fitting errors or dimensional issues.