DIY ELECTRONICSTR

← All projects

Joystick Pan-Tilt Camera Head

Beginner40 min

A camera head built from two servos: the bottom servo turns the camera left and right, the top one up and down, and you steer it with a joystick. The further you push the stick the faster the head turns, and it stays where it is when you let go; pressing the joystick sends both servos slowly back to the middle. The servos run from a separate 3 x AA battery holder, with the grounds joined. It teaches turning an analog stick into a speed with a dead zone, moving servos smoothly and within limits with millis(), and powering more than one servo from a supply of their own. The Servo library comes with the Arduino IDE; there is nothing to install.

Build it step by step in the app

What you need

Wiring steps

  1. 1

    Connect the Arduino's power wires

    Connect the Arduino's power wires

    Connect the 5V pin on the Arduino's power header to the breadboard's bottom red (+) rail with a red male-male wire, and the GND pin on the same header to the bottom blue (−) rail with a black wire. The bottom rails feed the joystick; the servos will run from a battery of their own.

    • Arduino GND → the bottom blue (−) rail (black wire)
    • Arduino 5V → the bottom red (+) rail (red wire)
  2. 2

    Plug in and wire the joystick

    Plug in and wire the joystick

    Plug the joystick module in with its five pins in row E of the bottom half: GND in column 4, +5V in 5, VRx in 6, VRy in 7 and SW in 8; a module whose pins point sideways stands upright on the breadboard. Connect column 4 to the bottom (−) rail with a short black wire and column 5 to the bottom (+) rail with a short red wire. Connect the Arduino's A2 pin to column 6 (VRx, left and right) with an orange wire, A1 to column 7 (VRy, up and down) with a purple wire, and A0 to column 8 (SW, the button) with a white wire (JOY_X, JOY_Y, JOY_BUTTON). The code gives the button its resistor with the Arduino's own pull-up.

    • joystick GND → column 4, bottom half
    • joystick +5V → column 5, bottom half
    • joystick VRx → column 6, bottom half
    • joystick VRy → column 7, bottom half
    • joystick SW → column 8, bottom half
    • column 4, bottom half → the bottom blue (−) rail (black wire)
    • column 5, bottom half → the bottom red (+) rail (red wire)
    • Arduino A2 → column 6, bottom half (orange wire)
    • Arduino A1 → column 7, bottom half (purple wire)
    • Arduino A0 → column 8, bottom half (white wire)
  3. 3

    Connect the battery holder, join the grounds

    Connect the battery holder, join the grounds

    Put 3 AA batteries in the holder and leave its switch OFF for now. Plug its red wire into the breadboard's top red (+) rail and its black wire into the top blue (−) rail. At the right end of the breadboard, join the bottom (−) rail to the top (−) rail with a black wire: unless the Arduino's and the battery's GND are joined, the servos cannot read the Arduino's signal. Do not join the two (+) rails: the top one carries the battery's 4.5V, the bottom one the Arduino's 5V. The servos get a battery of their own because two servos turning at once can draw enough current to pull the Arduino's 5V down and restart the board.

    • battery holder + lead → the top red (+) rail (red wire)
    • battery holder − lead → the top blue (−) rail (black wire)
    • the bottom blue (−) rail → the top blue (−) rail (black wire)
  4. 4

    Connect the pan servo

    Connect the pan servo

    Push three male-male wires into the servo's three-wire plug. Connect the orange (signal) wire to the Arduino's pin 10, the red (+) wire to the top red (+) rail and the brown (−) wire to the top blue (−) rail (PAN_PIN 10). This servo turns the camera left and right: put it at the bottom of the head with its shaft pointing up.

    • Arduino pin 10 → pan servo signal lead (orange wire)
    • pan servo + lead → the top red (+) rail (red wire)
    • pan servo − lead → the top blue (−) rail (brown wire)
  5. 5

    Connect the tilt servo

    Connect the tilt servo

    Push three male-male wires into the servo's three-wire plug. Connect the orange (signal) wire to the Arduino's pin 9, the red (+) wire to the top red (+) rail and the brown (−) wire to the top blue (−) rail (TILT_PIN 9). This servo turns the camera up and down: fix it on its side to the bottom servo's arm (with a pan-tilt bracket, hot glue or stiff card). Once everything is connected, switch the battery holder ON.

    • Arduino pin 9 → tilt servo signal lead (orange wire)
    • tilt servo + lead → the top red (+) rail (red wire)
    • tilt servo − lead → the top blue (−) rail (brown wire)

Arduino sketch

// The Servo library comes with the IDE
#include <Servo.h>

// Joystick: two axes and its push button
const int JOY_X = A2;
const int JOY_Y = A1;
const int JOY_BUTTON = A0;
// The servos' orange (signal) wires
const int PAN_PIN = 10;
const int TILT_PIN = 9;

// Angle limits: narrow them if the head
// catches on something or strains
const int PAN_MIN = 0;
const int PAN_MAX = 180;
const int TILT_MIN = 20;
const int TILT_MAX = 160;
const int CENTER = 90;
// Wobble this small at the middle is
// ignored
const int DEAD_ZONE = 60;
// Fastest turn (degrees per second)
const float MAX_SPEED = 90.0;
// If it turns against the way you push,
// set the matching one to true
const bool INVERT_PAN = false;
const bool INVERT_TILT = false;

Servo pan;
Servo tilt;
float panAngle = CENTER;
float tiltAngle = CENTER;
// Middle readings with the stick let go
int centerX = 512;
int centerY = 512;
// A press sends it back to the middle
bool homing = false;
unsigned long lastStep = 0;
unsigned long lastPrint = 0;
int shownPan = -1;
int shownTilt = -1;
int printedPan = -1;
int printedTilt = -1;

// How far the stick is pushed: -1 to 1,
// 0 inside the dead zone
float stick(int pin, int center) {
  int offset = analogRead(pin) - center;
  if (abs(offset) <= DEAD_ZONE) return 0;
  float range = 512 - DEAD_ZONE;
  float amount = (abs(offset) - DEAD_ZONE) / range;
  if (amount > 1) amount = 1;
  return offset > 0 ? amount : -amount;
}

// Moves the angle at most "step" degrees
// towards the target
float approach(float angle, float target, float step) {
  if (angle < target) return min(angle + step, target);
  return max(angle - step, target);
}

void setup() {
  Serial.begin(9600);
  pinMode(JOY_BUTTON, INPUT_PULLUP);
  // Each stick's middle is a little off:
  // measured at start (hands off)
  long sumX = 0;
  long sumY = 0;
  for (int i = 0; i < 16; i++) {
    sumX += analogRead(JOY_X);
    sumY += analogRead(JOY_Y);
    delay(5);
  }
  centerX = sumX / 16;
  centerY = sumY / 16;
  // A held stick would read wrong
  if (abs(centerX - 512) > 150) centerX = 512;
  if (abs(centerY - 512) > 150) centerY = 512;

  pan.attach(PAN_PIN);
  tilt.attach(TILT_PIN);
  pan.write(CENTER);
  tilt.write(CENTER);
  Serial.println("Ready: push the stick, press it to go back to the middle.");
  lastStep = millis();
}

void loop() {
  unsigned long now = millis();
  // 50 steps a second
  if (now - lastStep < 20) return;
  float seconds = (now - lastStep) / 1000.0;
  lastStep = now;

  float x = stick(JOY_X, centerX);
  float y = stick(JOY_Y, centerY);
  if (INVERT_PAN) x = -x;
  if (INVERT_TILT) y = -y;

  if (digitalRead(JOY_BUTTON) == LOW) homing = true;
  // Pushing the stick stops the way home
  if (x != 0 || y != 0) homing = false;

  float step = MAX_SPEED * seconds;
  if (homing) {
    panAngle = approach(panAngle, CENTER, step);
    tiltAngle = approach(tiltAngle, CENTER, step);
    if (panAngle == CENTER && tiltAngle == CENTER) homing = false;
  } else {
    // Pushed further, it turns faster
    panAngle = constrain(panAngle + x * step, PAN_MIN, PAN_MAX);
    tiltAngle = constrain(tiltAngle + y * step, TILT_MIN, TILT_MAX);
  }

  // Servos are written only on a change
  int panNow = round(panAngle);
  int tiltNow = round(tiltAngle);
  if (panNow != shownPan || tiltNow != shownTilt) {
    pan.write(panNow);
    tilt.write(tiltNow);
    shownPan = panNow;
    shownTilt = tiltNow;
  }
  // Angles at most every 0.2 s, and the
  // last one always
  bool changed = shownPan != printedPan || shownTilt != printedTilt;
  if (changed && now - lastPrint >= 200) {
    lastPrint = now;
    printedPan = shownPan;
    printedTilt = shownTilt;
    Serial.print("Pan: ");
    Serial.print(shownPan);
    Serial.print("°  Tilt: ");
    Serial.print(shownTilt);
    Serial.println("°");
  }
}

What the Serial Monitor shows

SERIAL MONITOR · 9600 BAUD
Ready: push the stick, press it to go back to the middle.
Pan: 90°  Tilt: 90°
Pan: 108°  Tilt: 90°
Pan: 126°  Tilt: 90°
Pan: 126°  Tilt: 72°

What you will see

Once the code is uploaded both servos go to the middle (90°). Push the joystick left or right: the bottom servo turns that way, faster the further you push, 90° a second at most. Let go and the servo stays where it is. Push up or down and the top servo turns; it stops between 20° and 160° so it is not strained. Press the joystick and both servos go slowly back to the middle; pushing the stick meanwhile stops that. The angles are also printed in the Serial Monitor. If the servos turn against the way you push, set INVERT_PAN or INVERT_TILT in the code to true. Keep your hands off the stick while the code starts: the Arduino measures the stick's middle then. If the servos do not move at all, check that the battery holder is switched ON and that the two (−) rails are joined by a wire; if they jitter or slow down, put in fresh batteries.

In the app, tick off every wire, match the parts to your inventory and pick up where you left off.

Build it step by step in the app