Project tutorial
Pan and Tilt Head with Very Slow and Fluid Movements

Pan and Tilt Head with Very Slow and Fluid Movements © GPL3+

I made this pan and tilt to film village theater from back of the room. Because of the high zoom rate, slow movements are necessary.

  • 1,744 views
  • 0 comments
  • 8 respects

Components and supplies

Necessary tools and machines

Apps and online services

Ide web
Arduino IDE
Pan_and_Tilt_Head.ino and PrintAlign.ino must be copied to a subdirectory "Pan_and_Tilt_Head" before compiling

About this project

I made this pan and tilt to film the village theater from the back of the room. Due to the high zoom rate, slow movements are required.

As there is already a high density of wifi and bluetooth in this room, I have chosen to use a wired connection to avoid disturbances. The handiest way was to use an RJ45 ethernet cable (e.g. cat 5). The 4 pairs of wires carry the motor control signals, ground and power.

A Hall sensor joystick allows the camera to be positioned at a speed proportional to the movement of the stick. The speed is also conditioned by the position of the speed potentiometer. It ranges from 1 (slowest) to 255 (fastest). In the video below, the potentiometer is set to maximum speed.

The stepper motor drivers are mounted on a perfboard, directly on the underside of the pan and tilt base. The arduino, the display and the control devices are in a separate control box.

The control box

A Ky-023 joystick for Arduino gave no satisfaction because it was not precise enough. So I switched to a Hall sensor joystick that can be found on eBay under "Jumper Hall Sensor Gimbal"

The joystick, LCD display, speed pot and backlight pot are mounted on the front plate. Only one RJ45 connector is on the side of the box.

Inside the control box, the regulators, the Arduino and other components are soldered and wire connected on a perfboard. I used some SMD parts and an Arduino Pro Mini because there was not enough room for the Nano in the case.

But with the Pro Mini there is a problem: the 3.3V will be the reference for the ADC but the VREF pin is not connected. To overcome to this problem, I cut the track going to the RST pin and connected pin 98 from the ATMEGA328P to this freed pin. This is very tricky if you are not used to solder on such small components as the Atmega328p chip. I prefer to advise you to use an Arduino Nano instead...

Update

In some situations the speed increase is growing too quickly at the end of the joystick stroke. So I added a switch according to the state of which the equation is changed dynamically in the procedure "CalculateSpeed". The position of the switch is displayed on the LCD screen (Lin. Fa. or Sl.), but it is only refreshed when a movement is requested by the joystick.

With this 3 position SPDT switch "ON-OFF-ON" (center is OFF), the choice between 3 equations is now possible :

  • exponential - Slow growth at the beginning (same than in first version)
  • exponential - slightly Faster growth in the beginning
  • linear

See the graph below for the motor speed relationship as a function of the joystick position for each of the 3 equations.

The finished box looks like this now with the switch between the two potentiometers :

The pan & tilt mechanical basis

The drive is made with NEMA 14 (tilt) and NEMA 16 (pan) stepper motors of 0.9° per step. They are driven with a 1/16 micro-step. In order to achieve a slow and smooth motion, additional reduction is performed by pulleys and timing belts. The reduction ratio is about 15 to 160 teeth for the pan and 15 to 60 teeth for the tilt (the latter should have had 120 teeth instead of 60, but the pulley that was delivered to me was too damaged to use it). As no 160 teeth timing pulley was available, I machined a smooth pulley as big as possible (Ø 102mm) in a recovery piece of PVC. The drive is done by friction with the timing belt. A roller on an articulated arm and a spring keeps this belt taut. A small elastic metal bracket allows blocking the arm to release the tension of the belt when it is not used.

For the panoramic axis, I used a bearing assembly of a VCR playback head. I had made a mock-upwith a 3.5" hard drive spindle bearing, but I found it undersized for the camera I had (about 800g). I have since switched to a 4K camera that weighs only 400g. So the hard drive bearing might have been enough...

For end of rotation the aluminum plates mounted in the profile come to stop on the motor shaft. (see on the pictures below).

Two ball bearings were fitted with a spacer in the vertical profile for the "tilt" movement. The one outside is barely visible on the left picture. The black M8 torx screw is mounted inside. It fixes the notched pulley with the black arm. The stepper motor is fixed on a mobile carriage. A Wing Nut ensures the blocking after tensioning the belt manually. The white nylon stop on the motor carriage ensures rotation blocking with the black tilt arm.

A screw thread for a photo mount (Kodak pitch: ¼" UNC - 20 tpi) is inserted in the bottom plate, nearly vertical to the center of gravity.

Spirit level, hinged plate for wall mounting and a stop to adjust horizontality allow for different types of installation. In particular wall mounting in the theater room.

The centers of gravity of the moving assemblies, with the camera mounted, are placed as close as possible to the axes of rotation. Thus, the required torque of the motors has been minimized. The optical axis is a little off center. In use, this is not annoying.

With the assembly of plastic plates and aluminum profiles of recovery, the aesthetics is a little "DIY" but it works rather well. I worked without mechanical plans, with what I could find to recycle...

As a final improvement, I replaced the too noisy two DRV8825 by two AT2100 while waiting for the delivery of new TMC2208 (I had broken the first ones with a reverse polarity power supply). I can't recommend the use of these AT2100 drivers: there is no documentation on this chip and, on the other side, the micro-step ratio of 1/16 can not be modified if faster mooving shall be required...

The software

The software itself is a little bit tricky. Because of time consuming exponential calculations (something like ((x/40)^2)+ 0.1x), AccelStepper is not called in "loop()" but in a timer interrupt. This allow smooth and "fast" running of the stepper motors without hicking.

I have tried several equations for the calculation of displacement according to the position of the joystick (linear, exponential, first degree, second degree, cubic). The second degree equation was the one that seemed to me to were the most pleasant in the proportionality of the displacements. But you can choose which you like most or create your own one. You only need to change the equation in the function "CalculateSpeed". Commented out you can find there two other equations I tested.

Version 1.2 of the software. I mainly changed the setPinsInverted () of the two stepper motors because with the TMC2208 the direction of rotation is reversed. I also increased the joystick deadzone in tilt.

Before compiling the arduino software, you must copy to a subdirectory "Pan_and_Tilt_Head" these both files:

  • "Pan_and_Tilt_Head.ino

Version 2.1 of the software:

Only changed the file "Pan_and_Tilt_Head.ino".The file "PrintAlign.ino" remain the same.

New in "Pan&Tilt v2.1":

  • Replaced some "#define" with "const"
  • Serial.print are commented out in the "loop" to gain compute time
  • There was error in the formula: value was first divided and then squared instead of the reverse. The formula is now corrected and the factors are updated (the curve is the same than before)
  • In some situations the speed increase is growing too quickly at the end of the joystick stroke. A 3-position switch now allows the choice between 3 equations.

Note: the position of the switch is displayed on the LCD screen (Lin. Fa. Or Sl.) but is only refreshed when a movement is requested by the joystick.

*/

Code

Pan_and_Tilt_Head (outdated)Arduino
This is version 1.2 of the software. I mainly changed the setPinsInverted () of the two stepper motors because with the TMC2208 the direction of rotation is reversed. I also increased the joystick deadzone in tilt
//         ******************************************
//         *    Pan and Tilt Head for camcorder     *
//         *    Daniel Engel         31/08/2020     *
//         ******************************************
//
//  Please be indulgent about my clumsiness with syntax and coding :
//  I don't speak very well english and I am a newbie in programming.
//
/*
   Stepper motors speed and displacement direction are controlled by a joystick.
   An additional potentiometer adjusts the speed range. A second order equation gives
   better precision for low displacement speeds. As it is very costly in computation
   time, the stepping is initiated in an interrupt routine triggered by timer 2.

   Unfortunately there is hidden snag due to non symetrical joystick voltage excursion.
   Due to the exponential relation the maximum speeds are quite different in each way.
   It might be possible to modify the software to calibrate the joystick voltage
   values at startup and remap them to symmetrical values.


   The stepper motors I have used are 0.9 per step. They are driven in 1/16 step.
   Since the camcorder should shoot at high zoom so additionnal reduction was needed
   for smooth movement. The drive is done by timing pulleys and belts with a reduction
   ratio of 15 to 60 teeth for the tilt and 15 to 160 teeth for the panning.
   The 160 tooth timing pulley was not readily available. A smooth pulley was machined
   in a pvc piece on a lathe. The drive is done by friction with the timing belt.

   Many thanks to Mike McCauley for his AccelStepper library for Arduino
      https://github.com/waspinator/AccelStepper/

   and Frank de Fmap for "Arduino-LiquidCrystal-I2C-library"
      https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

   =================================================================================
   Hardware that I've used
   =================================================================================

   The mechanism is made so that the center of gravity with mounted camcorder is very
   close to the axes of rotation. So everything can turn with minimal resistance.
   The ball bearings were recovered from old 3.5" hard drives.

   -------------------------------------------------------
   >>>  Arduino UNO or Arduino NANO (or Pro Mini with modification to bring out Vref pin)
   >>>  CNC Shield for Stepper Motor Driver
   >>>  TMC2208 V1.2 Stepper Driver Module (the quietest driver I have tried)
   >>>  16x2 Character LCD Display with I2C interface
   >>>  50 KOhm linear potentiometer for speed range
   >>>  50 KOhm linear potentiometer for backlight brightness
        (with 100nF capacitors between Arduino's analog inputs and ground)
   >>>  5V regulator (e.g. 7805 or 78L05)
   >>>  12V-3A Power Adapter

   -------------------------------------------------------
   >>>  Vertical Stepper motor (tilt): STH-39C8011-03  <<<
   -------------------------------------------------------
      0.9 degr Nema 16 (39*39mm, height: 19.5mm)
      Resistance: 5.3 ohms
      Torque: 0.13 N.m @ 0.98A (in the real wolrd only abour 2/3 of that...)
      Weight: 98g
   Since the motor gets quite hot at this current and the torque is more than enough
   for my camcorder (weight is about 420g), the current is set to the double of minimum
   needed for sufficient torque, say 0.4A .............???

   -------------------------------------------------------
   >>>  Horizontal Stepper motor (pan): 14H33HM-0404A2  <<<
   -------------------------------------------------------
      0.9 degr Nema 14 (35*35mm, height: 27.5mm)
      Resistance per Phase ~6.5 ohms
      Torque: 0.15 N.m @ 0.82A (same remark as above about the real torque !)
      Weight: 132g
   Current is set to the minimum needed for sufficient torque, say 0.4A .............???

   -------------------------------------------------------
   >>>  Jumper Hall Gimbal Product Code: JMP-P10107  <<<
   -------------------------------------------------------
 	  Supply Voltage: DC 3.0 ~ 3.5V   (3.3V is fine)
      Pay attention to the connection. On mine the wiring is a bit strange:
      the  red wire is GND, the black wire is + 3.3V and the yellow wire is Vout.

         Joystick position      analogRead / (measured voltage (Aref = 3.3V)
                                Horizontal         Vertical
                 min	          200 / (0.61V)     197 / (0.56V)
                center	        577 / (1.63V)     570 / (1.61V)
                 max 	          950 / (2.65V)     905 / (2.55V)

     The voltage excursion is not symmetrical for movements in min and max directions.

*/

#include <math.h>
#include <AccelStepper.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);     //  address 0x27 - / 2 line - 16 chars LCD display
//LiquidCrystal_I2C lcd(0x3f, 16, 2);   //  address 0x3f

//#define VersionMsg "Pan&Tilt MiniPro"
#define VersionMsg "Pan&Tilt v1.2"     // English translation + improved Horizontal deadZone

#define joyHorizontal A0    // Joystick X pin
#define joyVertical A1      // Joystick Y pin
#define speedPot A3         // Master speed potentiometer
#define lightSensor A2      // Light Sensor (in fact it's another potentiometer)
#define step_pulse_X 3      // Horizontal  Axis
#define step_pulse_Z 2      // Vertical  Axis
#define direction_X 5       // Horizontal  Axis
#define direction_Z 4       // Vertical  Axis
// #define enable 8            // *** LOW TO ENABLE DRV8825 DRIVERS OUTPUTS ***
//                             (if enable is not wired, it can be left floating)
#define pwmLCD 9              // PWM pin for LCD backlight (see I2C interface modification diagram)
// #define horizontal_LED 10  // N.A.
// #define vertical_LED 11    // N.A.
// LED_BUILTIN 13             // N.A.
//
// Define the stepper motors and the used pins  (Type of driver, STEP_PIN, DIR_PIN)
AccelStepper stepperHorizontal(1, step_pulse_X, direction_X);   // HORIZONTAL (X Axis on the stepper shield)
AccelStepper stepperVertical(1, step_pulse_Z, direction_Z);     // VERTICAL (Z Axis on the stepper shield))

int joyHorizontalPos = 0;
int joyVerticalPos = 0;
int PotValue = 0;                 // "Speed" potentiometer
int restHorizontal = 0;           // Joystick rest position
int restVertical = 0;
int deadZoneHorizontal = 40;      // Dead zone (without movement)
int deadZoneVertical = 100;       // On my joystick, less than 80 lead to errors when moving down
float scaleHorizontal = 8;        // Divider to map to full speed (scale is in the denominator)
float scaleVertical = 32;         // Divider to map to full speed (smaller value => greater speed)
//                                   (The vertical should move more slowly)
int temp = 0;
float hSpeedValue = 0;
float vSpeedValue = 0;

volatile unsigned long duree;     // => only used for testing (cycle time measurement)

byte customCharUp[8] = {          // up arrow
  B00000,
  B00000,
  B00100,
  B01110,
  B11011,
  B10001,
  B00000,
  B00000
};
byte customCharDown[8] = {        // down arrow
  B00000,
  B00000,
  B00000,
  B00000,
  B10001,
  B11011,
  B01110,
  B00100
};


void setup() {

  /*   The direction of the motion can be inverted with following instruction:
       stepperX.setPinsInverted( direction, step, enable);     // as boolean (true / false)
       another way is simply to reverse the motor connectors
  */
  //   Note that the direction of rotation is reversed with TMC2208 drivers

  stepperHorizontal.setPinsInverted( true, false, false);     // direction is inverted
  stepperVertical.setPinsInverted( false, false, false);
  // Set initial seed values for the steppers
  stepperHorizontal.setMaxSpeed(4000);
  stepperHorizontal.setAcceleration(200.0);
  stepperHorizontal.setSpeed(200);
  stepperVertical.setMaxSpeed(4000);
  stepperVertical.setAcceleration(200.0);
  stepperVertical.setSpeed(200);
  //  pinMode(vertical_LED, OUTPUT);
  //  pinMode(horizontal_LED, OUTPUT);
  //  pinMode(enable, OUTPUT);
  //  digitalWrite(enable, LOW);

  Serial.begin (115200);
  Serial.println ("*** Test AccelStepper with ISR on timer2 (~125s ***)");

  lcd.begin();                          // initialize the LCD
  lcd.noBacklight();                    // lcd.backlight(); shall be "OFF" so that the PWM can works
  lcd.createChar(6, customCharUp);      // down arrow at CGRAM(7)
  lcd.createChar(7, customCharDown);    // down arrow at CGRAM(7)
  lcd.home();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(VersionMsg);
  Serial.println (VersionMsg);
  delay (2000);

  // lcd.print("Repos potentios");
  //         1234567890123456 (LCD 2 lignes 16 caractres)

  analogReference(EXTERNAL);  // the 3V3 is connected to Aref with a resistance of 1K (recommended value is 5K)
  delay (250);
  PotValue = analogRead(speedPot);      // Dummy read (first read may be false after analogReference change)
  delay (250);
  PotValue = analogRead(speedPot);                  //  Speed multiplier (not used this time - Value is updated in loop() )
  restHorizontal = analogRead(joyHorizontal);       // Joystick X - Pan movement
  restVertical = analogRead(joyVertical);           // Joystick Y - Tilt movement

  TCCR2B = 0x00;                        // Disable Timer2 while setting up
  TIFR2  = 0x00;                        // Timer2 INT Flag Reg: Clear Timer Overflow Flag
  TIMSK2 = 0x01;                        // Timer2 INT Reg: Timer2 Overflow Interrupt Enable
  TCCR2A = 0x00;                        // Timer2 Control Reg A: Wave Gen Mode normal

  /*   Best compromise for interruptions between Fmax and regularity of rotation
    => shortening this time is done to the detriment of the time available for the calculation
  */
  TCCR2B = 0x02;                        // Timer2 Prescaler set to 8 (~250s)
  TCNT2  = 12;                          // -> do not forget to add it in the ISR !!!

  /* Setting low bits of TCCR2B  //  TCCR2B = (TCCR2B & 0b11111000) | <setting>;
        Setting   Divisor   Frequency
          0x01      1       31372.55
          0x02      8       3921.16
          0x03     32       980.39
          0x04     64       490.20   <--DEFAULT PWM
          0x05     128      245.10
          0x06     256      122.55
          0x07    1024      30.64
  */
  lcd.setCursor(0, 0);
  //              1234567890123456 (for positionning in the LCD 16 chars line)
}

void loop() {
  lcd.setCursor(0, 0);
  //          1234567890123456 (for positionning in the LCD 16 chars line)
  lcd.print ("Horiz.Vert. Pot.");
  PrintMove();
  PotValue = analogRead(speedPot);                //  Speed multiplier
  PotValue = map(PotValue, 0, 1023, 10, 255 );    // map(value, fromLow, fromHigh, toLow, toHigh)

  // => PWM for LCD backlight ~100mA with "analogWrite(160)
  // Change the "map" value to meet your requirements
  // for modern LCD backlights 20 to 30 mA shall be enough
  // To limit current the jumper on the I2C interface can be replaced by a 100 to 470 Ohm resistor
  analogWrite(pwmLCD, map(analogRead(lightSensor), 0, 1023, 10, 120 ));
  
  lcd.setCursor(12, 1);
  lcdPrintAlign(PotValue, 3,  ' ');

  //  *******************************************************************
  //  Joystick X - Pan movement (Horizontal)
  //  *******************************************************************

  joyHorizontalPos = analogRead(joyHorizontal);   // Joystick X - Pan movement

  // if Joystick is moved left, move Horizontal stepper - pan to left
  if (joyHorizontalPos > (restHorizontal  + deadZoneHorizontal)) {
    hSpeedValue = (joyHorizontalPos - (restHorizontal + deadZoneHorizontal));
    hSpeedValue = CalculateSpeed ((float)hSpeedValue, (float)PotValue, (float)scaleHorizontal );
  }
  // if Joystick is moved right, move Horizontal stepper - pan to right
  else if (joyHorizontalPos < (restHorizontal - deadZoneHorizontal)) {
    hSpeedValue = ((restHorizontal - deadZoneHorizontal) - joyHorizontalPos);
    hSpeedValue = -CalculateSpeed ((float)hSpeedValue, (float)PotValue, (float)scaleHorizontal );
  }
  // if Joystick stays in middle, no movement
  else {
    hSpeedValue = 0;
  }
  stepperHorizontal.setSpeed(hSpeedValue);
  //  Serial.print("h_Speed: ");
  //  Serial.println(hSpeedValue);


  //  *******************************************************************
  //  Joystick Y - Tilt movement (Vertical)
  //  *******************************************************************
  // Note: Joystick Y is mounted reversed way
  joyVerticalPos = analogRead(joyVertical);       // Joystick Y - Tilt movement

  if (joyVerticalPos < (restVertical - deadZoneVertical)) {
    vSpeedValue = joyVerticalPos - (restVertical + deadZoneVertical);
    vSpeedValue = CalculateSpeed ((float)vSpeedValue, (float)PotValue, (float)scaleVertical );
  }
  else if (joyVerticalPos >  (restVertical  + deadZoneVertical)) {
    vSpeedValue = (restVertical - deadZoneVertical) - joyVerticalPos;
    vSpeedValue = -CalculateSpeed ((float)vSpeedValue, (float)PotValue, (float)scaleVertical );
  }
  else {
    vSpeedValue = 0;
  }
  stepperVertical.setSpeed(vSpeedValue);
}

//  *************************************************************************
//  Calculate speed (The last tested quadratic equation seems most pleasant)
//       Note: when calling the "int" are converted into "float"
//  *************************************************************************

float CalculateSpeed (float value, float factor , float scale) {
  //   ... choice to be made between the three equations ...
  //  float valeur = ((sq(value / 35) * factor) / scale);                 // (x/35)^2
  //  float valeur = ((pow(value / 70 , 3)) * factor) / scale;            // (x/70)^3
  //  ... And at last the most pleasant to use in my experience ...
  float valeur = (((sq(value / 40) + (value * 0.1)) * factor) / scale);   //  ((x/40)^2)+ 0.1x

  return valeur;
}


//  *************************************************************************
//  Display motion direction
//  *************************************************************************
void PrintMove() {

  // Visualize Horizontal motion
  lcd.setCursor(1, 1);
  if (hSpeedValue < 0)  {
    lcd.print (">>>");
  }
  else if (hSpeedValue > 0)  {
    lcd.print ("<<<");
  }
  else {
    lcd.print ("   ");
  }

  // Visualize Vertical motion
  lcd.setCursor(6, 1);
  if (vSpeedValue < 0)  {
    for (byte i = 0; i <= 2; i++) {
      lcd.write(6);;      // customChar "up arrow"
    }
  }
  else if (vSpeedValue > 0)  {
    for (byte i = 0; i <= 2; i++) {
      lcd.write(7);;      // customChar "down arrow" could be replaced with: lcd.print ("vvv");
    }
  }
  else {
    lcd.print ("   ");
  }
}

//  *************************************************************************
//  This is now the heart and secret for smooth and fast running
//  stepper motors with the AccelStepper library
//  *************************************************************************
ISR(TIMER2_OVF_vect) {
  TCNT2  = 12;
  stepperHorizontal.runSpeed();
  stepperVertical.runSpeed();
}
PrintAlignArduino
// *******************************************************************
// ************ LCD display 2 digits/numbers  ************************
// Display 16 bit unsigned integers as number and fill mising digits
// with any character if length less than specified by digit
//    ---  maximum value is 2^16, or 65535 ---
// does fill leading missing digits but don't truncate longer number
//        number  : number to display
//        digit   : how much digits
//        fill    : characters to add on the left
// *******************************************************************
void lcdPrintAlign(uint16_t number, byte digit, char fill)
{
  switch (digit)
  {
    //    case 1:         //  1 digits not needed > goes to default
    //    goto Prior;
    //    break;
    case 2:
      goto Secundus;       //  2 digits
      break;
    case 3:
      goto Tertius;      //  3digits
      break;
    case 4:
      goto Quartus;  //  4 digits
      break;
    case 5:
      goto Quintus;   //  5 digits
      break;
    default:          //  not expected
      goto Prior;      //  but print as is
      break;
  }
Quintus:
  if (number < 10000) lcd.print(fill);
Quartus:
  if (number < 1000) lcd.print(char(fill));
Tertius:
  if (number < 100) lcd.print(char(fill));
Secundus:
  if (number < 10) lcd.print(char(fill));
Prior:
  lcd.print(number);
}
Pan_and_Tilt_HeadArduino
This is the updated file (v2.1). It replaces the first. The added 3-position switch allows now the choice between 3 equations
//         ******************************************
//         *    Pan and Tilt Head for camcorder     *
//         *    Daniel Engel         22/08/2020     *
//         ******************************************
//
//  Please be indulgent about my clumsiness with syntax and coding :
//  I don't speak very well english and I am a newbie in programming.
//
/*
   Stepper motors speed and displacement direction are controlled by a joystick.
   An additional potentiometer adjusts the speed range. A second order equation gives
   better precision for low displacement speeds. As it is very costly in computation
   time, the stepping is initiated in an interrupt routine triggered by timer 2.

   Unfortunately there is hidden snag due to non symetrical joystick voltage excursion.
   Due to the exponential relation the maximum speeds are quite different in each way.
   It might be possible to modify the software to calibrate the joystick voltage
   values at startup and remap them to symmetrical values.

   The stepper motors I have used are 0.9 per step. They are driven in 1/16 step.
   Since the camcorder should shoot at high zoom so additionnal reduction was needed
   for smooth movement. The drive is done by timing pulleys and belts with a reduction
   ratio of 15 to 60 teeth for the tilt and 15 to 160 teeth for the panning.
   The 160 tooth timing pulley was not readily available. A smooth pulley was machined
   in a pvc piece on a lathe. The drive is done by friction with the timing belt.

   Many thanks to Mike McCauley for his AccelStepper library for Arduino
      https://github.com/waspinator/AccelStepper/

   and Frank de Fmap for "Arduino-LiquidCrystal-I2C-library"
      https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

   =================================================================================
   Hardware that I've used
   =================================================================================

   The mechanism is made so that the center of gravity with mounted camcorder is very
   close to the axes of rotation. So everything can turn with minimal resistance.
   The ball bearing assembly for the panning was recovered from a old VCR head.
   For the tilt, two ball bearings were mounted in an aluminium profile.
   ---------------------------------------------------------------------------------
   >>>  Arduino NANO (or Pro Mini with modification to bring out Vref pin)
   >>>  CNC Shield for Stepper Motor Driver
   >>>  TMC2208 V1.2 Stepper Driver Module (the quietest driver I have tried)
         drv8825 will also work but they are quite loud...
   >>>  16x2 Character LCD Display with I2C interface
   >>>  50 KOhm linear potentiometer for speed range
   >>>  50 KOhm linear potentiometer for backlight brightness
        (with 100nF capacitors between Arduino's analog inputs and ground)
   >>>  5V regulator (e.g. 7805 or 78L05)
   >>>  12V-3A Power Adapter

   >>>  NEW: a 3 position SPDT switch Centre Off (ON-OFF-ON)

   -------------------------------------------------------
   >>>  Vertical Stepper motor (tilt): STH-39C8011-03  <<<
   -------------------------------------------------------
      0.9 degr Nema 16 (39*39mm, height: 19.5mm)
      Resistance: 5.3 ohms
      Torque: 0.13 N.m @ 0.98A (in the real wolrd only abour 2/3 of that...)
      Weight: 98g
   Since the motor gets quite hot at this current and the torque is more than enough
   for my camcorder (weight is about 420g), the current is set to the double of minimum
   needed for sufficient torque, say 0.4A ....???

   -------------------------------------------------------
   >>>  Horizontal Stepper motor (pan): 14H33HM-0404A2  <<<
   -------------------------------------------------------
      0.9 degr Nema 14 (35*35mm, height: 27.5mm)
      Resistance per Phase ~6.5 ohms
      Torque: 0.15 N.m @ 0.82A (same remark as above about the real torque !)
      Weight: 132g
   Current is set to double of min needed for sufficient torque, (0.4A)

   -------------------------------------------------------
   >>>  Jumper Hall Gimbal Product Code: JMP-P10107  <<<
   -------------------------------------------------------
 	  Supply Voltage: DC 3.0 ~ 3.5V   (3.3V is fine)
      Pay attention to the connection. On mine the wiring is a bit strange:
      the  red wire is GND, the black wire is + 3.3V and the yellow wire is Vout.

         Joystick position      analogRead / (measured voltage (Aref = 3.3V)
                                Horizontal         Vertical
                 min	          200 / (0.61V)     197 / (0.56V)
                center	        577 / (1.63V)     570 / (1.61V)
                 max 	          950 / (2.65V)     905 / (2.55V)

    The voltage excursion is not completely symmetrical for movements in the min and max directions.

  New in "Pan&Tilt v2.1":
  
    - Replaced some "#define" with "const".
    - Serial.print are commented out in the "loop"
    - There was error in the formula: value was first divided and then squared instead of the reverse
      The formula is now corrected and the factors are updated (the curve is the same than before).
    - In some situations the speed increase is growing too quickly at the end of the joystick stroke.
      So I added a switch according to the state of which the equation is changed dynamically in the
      procedure "CalculateSpeed".
      Note: the position of the switch is displayed on the LCD screen (Lin. Fa. Or Sl.)
            but is only refreshed when a movement is requested by the joystick.
*/

#include <math.h>
#include <AccelStepper.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);     //  address 0x27 - / 2 line - 16 chars LCD display
//LiquidCrystal_I2C lcd(0x3f, 16, 2);   //  address 0x3f

#define VersionMsg "Pan&Tilt v2.1"     //

const byte joyHorizontal = A0;    // Joystick X pin
const byte joyVertical = A1;      // Joystick Y pin
const byte speedPot = A3;         // Master speed potentiometer
const byte lightSensor = A2;      // Light Sensor (in fact it's another potentiometer)
const byte step_pulse_X = 3;      // Horizontal  Axis
const byte step_pulse_Z = 2;      // Vertical  Axis
const byte direction_X = 5;       // Horizontal  Axis
const byte direction_Z = 4;       // Vertical  Axis
const byte pwmLCD = 9 ;           // PWM pin for LCD backlight (see I2C interface modification diagram)

/*  3 position switch added to select the equation in "CalculateSpeed"

         selectLinear      selectLaw     Response to joystick movement
            closed          Open           linear
             Open           Open           exponential - slightly Faster growth in the beginning
             Open          closed          exponential - Slow growth at the beginning
*/
const byte selectLaw = 7;         // Added for selecting equation in CalculateSpeed()
const byte selectLinear = 6;

// Define the stepper motors and the used pins  (Type of driver, STEP_PIN, DIR_PIN)
AccelStepper stepperHorizontal(1, step_pulse_X, direction_X);   // HORIZONTAL (X Axis on the stepper shield)
AccelStepper stepperVertical(1, step_pulse_Z, direction_Z);     // VERTICAL (Z Axis on the stepper shield))

int joyHorizontalPos = 0;
int joyVerticalPos = 0;
int PotValue = 0;                 // "Speed" potentiometer
int restHorizontal = 0;           // Joystick rest position
int restVertical = 0;
int deadZoneHorizontal = 40;      // Dead zone (without movement)
int deadZoneVertical = 100;       // On my joystick, less than 80 lead to errors when moving down
float scaleHorizontal = 8;        // Divider to map to full speed (scale is in the denominator)
float scaleVertical = 32;         // Divider to map to full speed (smaller value => greater speed)
//                                   vertical (tilt) should move more slowly than horizontal (pan)
float hSpeedValue = 0;
float vSpeedValue = 0;

// byte portD;                    // selection pins for the equation used are 6 and 7
const byte mask =  B00000011;     // used after bit shift (portD, bits 6 and 7) to invert the logic

byte customCharUp[8] = {          // up arrow
  B00000,
  B00000,
  B00100,
  B01110,
  B11011,
  B10001,
  B00000,
  B00000
};
byte customCharDown[8] = {        // down arrow
  B00000,
  B00000,
  B00000,
  B00000,
  B10001,
  B11011,
  B01110,
  B00100
};


void setup() {
  
  /*   The direction of the motion can be inverted with following instruction:
   *   stepperX.setPinsInverted( direction, step, enable);     // as boolean (true / false)
   *   another way is simply to reverse the motor connectors
  */
  // Note that the direction of rotation is reversed with TMC2208 drivers instead of DRV8825
  stepperHorizontal.setPinsInverted( true, false, false);     // Pan direction is inverted
  //stepperHorizontal.setPinsInverted( false, false, false);     // Pan direction is inverted
  stepperVertical.setPinsInverted( true, false, false);
  //stepperVertical.setPinsInverted( false, false, false);
  // Set initial speed values for the steppers
  stepperHorizontal.setMaxSpeed(4000);
  stepperHorizontal.setAcceleration(200.0);
  stepperHorizontal.setSpeed(200);
  stepperVertical.setMaxSpeed(4000);
  stepperVertical.setAcceleration(200.0);
  stepperVertical.setSpeed(200);

  pinMode(selectLaw, INPUT_PULLUP);
  pinMode(selectLinear, INPUT_PULLUP);

  Serial.begin (115200);
  Serial.println ("*** Test AccelStepper with ISR on timer2 (~125s ***)");

  lcd.begin();                          // initialize the LCD
  lcd.noBacklight();                    // lcd.backlight(); shall be "OFF" so that the PWM can works
  lcd.createChar(6, customCharUp);      // down arrow at CGRAM(7)
  lcd.createChar(7, customCharDown);    // down arrow at CGRAM(7)
  lcd.home();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(VersionMsg);
  Serial.println (VersionMsg);
  delay (2000);

  analogReference(EXTERNAL);  // the 3V3 is connected to Aref with a resistor of 1K (recommended value is 5K)
  delay (250);
  PotValue = analogRead(speedPot);      // Dummy read (first read may be false after analogReference change)
  delay (250);
  PotValue = analogRead(speedPot);            //  Speed multiplier (not used this time - Value is updated in loop() )
  restHorizontal = analogRead(joyHorizontal); // Joystick X - Pan movement
  restVertical = analogRead(joyVertical);     // Joystick Y - Tilt movement

  TCCR2B = 0x00;                        // Disable Timer2 while setting up
  TIFR2  = 0x00;                        // Timer2 INT Flag Reg: Clear Timer Overflow Flag
  TIMSK2 = 0x01;                        // Timer2 INT Reg: Timer2 Overflow Interrupt Enable
  TCCR2A = 0x00;                        // Timer2 Control Reg A: Wave Gen Mode normal

  /*   This seems to be best compromise for interruptions between Fmax and regularity of rotation
       => shortening this time is done to the detriment of the time available for the calculation
  */
  TCCR2B = 0x02;                        // Timer2 Prescaler set to 8 (~255s)
  TCNT2  = 12;                          // -> do not forget to add it in the ISR !!!
  /* Setting low bits of TCCR2B  //  TCCR2B = (TCCR2B & 0b11111000) | <setting>;
        Setting   Divisor   Frequency
          0x01      1       31372.55
          0x02      8       3921.16
          0x03     32       980.39
          0x04     64       490.20   <--DEFAULT PWM
          0x05     128      245.10
          0x06     256      122.55
          0x07    1024      30.64
  */

  //      Pins 6 and 7 are used to select the equation to be used
  for (byte i = 6; i <= 7; i++) {   // never modify pins 0 and 1 (RX andTX)
    //    Serial.print (i);
    pinMode (i, INPUT_PULLUP);
  }
}

void loop() {
  lcd.setCursor(0, 0);
  //          1234567890123456    // (This line is usefull for positionning text in the LCD 16 chars line)
  lcd.print ("Horiz.Vert.Speed");   // Pot.
  PrintMove();                    //  Displays arrows to visualize motion direction

  PotValue = analogRead(speedPot);                //  Speed multiplier
  PotValue = map(PotValue, 0, 1023, 10, 255 );    // map(value, fromLow, fromHigh, toLow, toHigh)

  // => PWM for LCD backlight ~100mA with "analogWrite(160)
  // Change the "map" value to meet your requirements
  // for modern LCD backlights 20 to 30 mA shall be enough
  // To limit current the jumper on I2C interface can be replaced by a resistor (100 to 470 Ohm)
  analogWrite(pwmLCD, map(analogRead(lightSensor), 0, 1023, 10, 120 ));

  lcd.setCursor(13, 1);
  lcdPrintAlign(PotValue, 3,  ' ');

  //  *******************************************************************
  //      Joystick X - Pan movement (Horizontal)
  //  *******************************************************************
  joyHorizontalPos = analogRead(joyHorizontal);   // Joystick X - Pan movement

  // if Joystick is moved left, move Horizontal stepper - pan to left
  if (joyHorizontalPos > (restHorizontal  + deadZoneHorizontal)) {
    hSpeedValue = (joyHorizontalPos - (restHorizontal + deadZoneHorizontal));
    hSpeedValue = CalculateSpeed ((float)hSpeedValue, (int)PotValue, (float)scaleHorizontal );
  }
  // if Joystick is moved right, move Horizontal stepper - pan to right
  else if (joyHorizontalPos < (restHorizontal - deadZoneHorizontal)) {
    hSpeedValue = ((restHorizontal - deadZoneHorizontal) - joyHorizontalPos);
    hSpeedValue = -CalculateSpeed ((float)hSpeedValue, (int)PotValue, (float)scaleHorizontal );
  }
  // if Joystick stays in middle, no movement
  else {
    hSpeedValue = 0;
  }
  stepperHorizontal.setSpeed(hSpeedValue);
  //  Serial.print("h_Speed: ");
  //  Serial.println(hSpeedValue);

  //  *******************************************************************
  //      Joystick Y - Tilt movement (Vertical)
  //  *******************************************************************
  joyVerticalPos = analogRead(joyVertical);       // Joystick Y - Tilt movement
  /*
    if (joyHorizontalPos > (restHorizontal  + deadZoneHorizontal)) {
      hSpeedValue = (joyHorizontalPos - (restHorizontal + deadZoneHorizontal));
      hSpeedValue = CalculateSpeed ((float)hSpeedValue, (int)PotValue, (float)scaleHorizontal );
  */
  if (joyVerticalPos > (restVertical + deadZoneVertical)) {
    vSpeedValue = joyVerticalPos - (restVertical + deadZoneVertical);
    vSpeedValue = CalculateSpeed ((float)vSpeedValue, (int)PotValue, (float)scaleVertical );
  }
  /*
    else if (joyHorizontalPos < (restHorizontal - deadZoneHorizontal)) {
    hSpeedValue = ((restHorizontal - deadZoneHorizontal) - joyHorizontalPos);
    hSpeedValue = -CalculateSpeed ((float)hSpeedValue, (int)PotValue, (float)scaleHorizontal );
  */
  else if (joyVerticalPos < (restVertical  - deadZoneVertical)) {
    vSpeedValue = ((restVertical - deadZoneVertical) - joyVerticalPos);
    vSpeedValue = -CalculateSpeed ((float)vSpeedValue, (int)PotValue, (float)scaleVertical );
  }
  else {
    vSpeedValue = 0;
  }
  stepperVertical.setSpeed(vSpeedValue);
}

//  *************************************************************************
//  Calculate speed (The last tested quadratic equation seems most pleasant)
//       Note: when calling the "int" are converted into "float"
//  *************************************************************************

float CalculateSpeed (float value, float factor , float scale) {
  float tempValue;
  byte portD;

  portD = PIND;        // gets bits 6 and 7 in LSB
  portD = portD >> 6 ;           // gets bits 6 and 7 in LSB
  portD = portD ^ mask;         // bit inversion

  /*   The switch has 3 positions: "1 - 0 - 2" for " UP - MIDDLE - DOWN. So I choosed:
       linear is DOWN, MIDDLE is slow exponential growing and UP is fast exponential growing
       Note: The position of the switch is only refreshed on the LCD screen AFTER a movement !
  */

  lcd.setCursor(9, 1);
  switch (portD) {        // remember: bit 7 and 6 were shifted right to bit 1 and 0

    case 0:               // Exponential - FAST growing at the begin (tumbler switch is MIDDLE)
      //      Serial.print ("Case 0 - Slow :  ");
      //      Serial.print (value);
      //      Serial.print (" : ");
      lcd.print (" Fa.");                    // display "Fa." in front of "potvalue"
      tempValue = (((((sq(value)) / 3200) + (value * 0.198)) * factor) / scale);   // ((x^2)/3200)+ 0.198x
      //     tempValue = (((((sq(value)) / 1600) + (value * 0.1)) * factor) / scale);   // ((x^2)/1600)+ 0.1x
      break;

    case 1:               // Exponential - SLOW growing at the begin (tumbler switch is DOWN)
      //      Serial.print  ("Case 1 - Fast :  ");
      //      Serial.print (value);
      //      Serial.print (" : ");
      lcd.print (" Sl.");                    // display "Sl." in front of "potvalue"
      /*
         tempValue = (((sq(value / 40) + (value * 0.1)) * factor) / scale);   // ((x/40)^2)+ 0.1x

         There was an error in previous formula: the value is first divided and then squared.
         Below is the corrected with the updated factors.
         The results are the same but the reasoning was wrong ...
      */
      tempValue = (((((sq(value)) / 1600) + (value * 0.1)) * factor) / scale);   // ((x^2)/1600)+ 0.1x
      break;

    case 2:               // LINEAR law (tumbler switch is UP)
      //      Serial.print  ("Case 2 - Lin :  ");
      //      Serial.print (value);
      //      Serial.print (" : ");
      lcd.print ("Lin.");                    // display "Lin." in front of "potvalue"
      tempValue = (((value * 0.296) * factor) / scale);   // 0.296x
      break;

    case 3:
      //      Serial.println  ("Case 3");  // not used with a single 3 positions switch
      break;

    default:
      //      Serial.println ("There is an unforseen case !");
      break;
  }
  //-------------------------------------------------------------------------
  //  Serial.println (tempValue);
  return tempValue;
}


//  *************************************************************************
//  Display motion direction
//  *************************************************************************
void PrintMove() {

  // Visualize Horizontal motion
  lcd.setCursor(1, 1);
  if (hSpeedValue < 0)  {
    lcd.print (">>>");
  }
  else if (hSpeedValue > 0)  {
    lcd.print ("<<<");
  }
  else {
    lcd.print ("   ");
  }

  // Visualize Vertical motion
  lcd.setCursor(5, 1);
  if (vSpeedValue < 0)  {
    for (byte i = 0; i <= 2; i++) {
      lcd.write(6);;      // customChar "up arrow"
    }
  }
  else if (vSpeedValue > 0)  {
    for (byte i = 0; i <= 2; i++) {
      lcd.write(7);;      // customChar "down arrow" could be replaced with: lcd.print ("vvv");
    }
  }
  else {
    lcd.print ("   ");
  }
}

//  *************************************************************************
//  This is now the heart and secret for smooth and fast running
//  stepper motors with the AccelStepper library
//  *************************************************************************
ISR(TIMER2_OVF_vect) {
  TCNT2 = 12;
  stepperHorizontal.runSpeed();
  stepperVertical.runSpeed();
}

Schematics

Schema Pan_and_Tilt_Head
Pan_and_Tilt_Head_UPDATE

Comments

Similar projects you might like

Human Head to Robot Head

Project tutorial by jegatheesan

  • 6,800 views
  • 8 comments
  • 13 respects

Smart cane

by Saurabh Dhingra

  • 10,765 views
  • 25 comments
  • 46 respects

COVID-19 and PM10 Levels!

Project tutorial by Mario Soranno

  • 8,323 views
  • 5 comments
  • 18 respects

Wirlesse Laser Data Transmission(LIFI) (100b/second)

Project tutorial by Ahmed Ramzi Houalef

  • 3,153 views
  • 10 comments
  • 6 respects

IoT Using ESP8266-01 and Arduino

Project tutorial by Ahmed Ibrahim Ahmed

  • 57,203 views
  • 25 comments
  • 69 respects

How to Make a Gimbal

Project tutorial by Mission Critical

  • 13,002 views
  • 1 comment
  • 16 respects
Add projectSign up / Login