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

The whole system is built around an Arduino Nano circuit. The power is provided by a 12 V power source (in my demo, it is a battery) which is directly connected to the stepper motor driver. The power source is also connected to a voltage regulator which steps the voltage down to 5 V. This 5 V powers the rest of the electronics: MCU, OLED display, joystick, and the rotary encoder.

The whole system is built around an Arduino Nano circuit. The power is provided by a 12 V power source (in my demo, it is a battery) which is directly connected to the stepper motor driver. The power source is also connected to a voltage regulator which steps the voltage down to 5 V. This 5 V powers the rest of the electronics: MCU, OLED display, joystick, and the rotary encoder.



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

Leg with screw hole on the right

Leg with screw hole on the right

Leg with screw hole on the left

Leg with screw hole on the left

Motor mount

Motor mount

Belt tensioner

Belt tensioner

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.

Previous
Previous

Building a coil winder [Part 4] - Updated feeder mechanism

Next
Next

SZBK07 DC-DC converter with multiturn potentiometers