ESP32 Bench
Boards: ESP32, S2, S3, C3, C6
Flash and monitor: Web Serial
← All projects

Joystick pan-tilt turret

Steer two servos with a thumb joystick, like a camera turret. Add an MPU-6050 and a click of the stick switches to tilt steering: point your hand and the turret follows.

Starter30 min115200 baud
Board
Fig. 1. Pins this project uses on the ESP32-DevKitC V4, 38 pin. USB connector at the bottom.

Wiring

FromTo boardNote
Joystick +5V or VCC3V33V3, not 5 V: at 5 V the stick could send more than 3.3 V into the ESP32
Joystick GNDGND
Joystick VRxGPIO 34An ADC1 pin
Joystick VRyGPIO 35An ADC1 pin
Joystick SWGPIO 27The internal pull-up does the rest
Pan servo signalGPIO 18
Tilt servo signalGPIO 19
Both servos red5VFine from USB for two micro servos that move gently; use a separate 5 V supply if the board resets
Both servos brownGND
Optional MPU-6050 SDA and SCLGPIO 21 and 22VCC to 3V3, GND to GND. Found automatically at start-up

Tested on this board

Not run on this board yet. Treat the wiring and code as a first draft.

Firmware, Arduino sketch for ESP32 Dev Module

// Joystick pan-tilt turret: push the stick and two servos follow. Click the stick
// to switch to tilt mode, where an MPU-6050 in your hand steers instead (if one
// is wired; without it the button does nothing).
#include <Wire.h>
#include <ESP32Servo.h>

const int JOY_X = 34;     // ADC1 pins
const int JOY_Y = 35;
const int JOY_BTN = 27;   // the stick's push button (SW)
const int PAN_PIN = 18;
const int TILT_PIN = 19;
const int SDA_PIN = 21;  // optional MPU-6050 for tilt mode
const int SCL_PIN = 22;

const float DEADZONE = 0.08f;    // ignore the stick this close to centre (fraction of full throw)
const float MAX_SPEED = 120.0f;  // degrees per second at full throw
const int MIN_ANGLE = 10, MAX_ANGLE = 170;
const float TILT_GAIN = 1.5f;    // tilt mode: servo degrees per degree of hand tilt
const uint8_t MPU = 0x68;

Servo pan, tilt;
float panAngle = 90, tiltAngle = 90;
int centerX = 2048, centerY = 2048;
bool hasMpu = false, tiltMode = false;
unsigned long lastMs = 0, lastPrint = 0;

int averageRead(int pin) {
  long sum = 0;
  for (int i = 0; i < 16; i++) sum += analogRead(pin);
  return sum / 16;
}

// Stick position from -1 to 1, with the dead zone removed and the rest rescaled.
float axis(int pin, int center) {
  const int raw = averageRead(pin);
  const float span = raw >= center ? 4095 - center : center;
  float v = (raw - center) / (span > 0 ? span : 1);
  if (fabsf(v) < DEADZONE) return 0;
  v = (fabsf(v) - DEADZONE) / (1 - DEADZONE) * (v > 0 ? 1 : -1);
  return constrain(v, -1.0f, 1.0f);
}

bool readTilt(float& pitch, float& roll) {
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(MPU, (uint8_t)6) != 6) return false;
  const float ax = (int16_t)((Wire.read() << 8) | Wire.read()) / 16384.0f;
  const float ay = (int16_t)((Wire.read() << 8) | Wire.read()) / 16384.0f;
  const float az = (int16_t)((Wire.read() << 8) | Wire.read()) / 16384.0f;
  pitch = degrees(atan2f(-ax, sqrtf(ay * ay + az * az)));
  roll = degrees(atan2f(ay, az));
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(JOY_BTN, INPUT_PULLUP);
  analogSetPinAttenuation(JOY_X, ADC_11db);
  analogSetPinAttenuation(JOY_Y, ADC_11db);

  pan.setPeriodHertz(50);
  tilt.setPeriodHertz(50);
  pan.attach(PAN_PIN, 500, 2400);
  tilt.attach(TILT_PIN, 500, 2400);
  pan.write(90);
  tilt.write(90);

  // The stick rests at its centre now: remember where that is.
  delay(300);
  centerX = averageRead(JOY_X);
  centerY = averageRead(JOY_Y);

  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.beginTransmission(MPU);
  hasMpu = Wire.endTransmission() == 0;
  if (hasMpu) {
    Wire.beginTransmission(MPU);
    Wire.write(0x6B);  // PWR_MGMT_1: wake up
    Wire.write(0);
    Wire.endTransmission();
  }
  Serial.printf("Turret ready. MPU-6050 %s.\n", hasMpu ? "found: click the stick for tilt mode" : "not found: stick only");
  lastMs = millis();
}

void loop() {
  const unsigned long now = millis();
  const float dt = (now - lastMs) / 1000.0f;
  lastMs = now;

  // Click to switch modes, only when an MPU-6050 is there to steer with.
  if (hasMpu && digitalRead(JOY_BTN) == LOW) {
    tiltMode = !tiltMode;
    Serial.println(tiltMode ? "Tilt mode: steer by tilting the MPU-6050" : "Stick mode");
    while (digitalRead(JOY_BTN) == LOW) delay(10);
    delay(30);  // debounce the release
  }

  float pitch, roll;
  if (tiltMode && readTilt(pitch, roll)) {
    panAngle = 90 + roll * TILT_GAIN;
    tiltAngle = 90 + pitch * TILT_GAIN;
  } else {
    // Stick mode: the further you push, the faster it turns, and it stays put when you let go.
    panAngle += axis(JOY_X, centerX) * MAX_SPEED * dt;
    tiltAngle += axis(JOY_Y, centerY) * MAX_SPEED * dt;
  }
  panAngle = constrain(panAngle, (float)MIN_ANGLE, (float)MAX_ANGLE);
  tiltAngle = constrain(tiltAngle, (float)MIN_ANGLE, (float)MAX_ANGLE);
  pan.write((int)roundf(panAngle));
  tilt.write((int)roundf(tiltAngle));

  if (now - lastPrint >= 150) {
    lastPrint = now;
    Serial.printf("pan = %d, tilt = %d\n", (int)roundf(panAngle), (int)roundf(tiltAngle));
  }
  delay(15);
}

Parts

QuantityPartWhere to buy
1×ESP32 dev boardBuy at Amazon
1×Analog thumb joystick with select buttonBuy at Adafruit
2×Micro servos, SG90 or similarBuy at Adafruit
1×Optional: MPU-6050 board (GY-521) for tilt steering
1×Optional: a pan-tilt bracket, or tape the tilt servo to the pan servo's horn
10×Jumper wires

Amazon buy links may include an Associates tag, and this site may earn a commission from qualifying Amazon purchases at no extra cost to you. Adafruit links are direct.

Libraries

Wire (built in), ESP32Servo

Load it

  1. In the Arduino IDE, install the esp32 board package and pick the board named above the sketch.
  2. Install the libraries listed above from the Library Manager.
  3. Paste the sketch, choose the board's port and upload. Hold BOOT if the upload stalls at Connecting.
  4. Close the IDE's serial monitor, then open the bench here to watch the output.

Field notes

  • Leave the stick alone while the board starts: it reads the resting position as the centre, so the turret does not creep.
  • In stick mode the turret moves faster the further you push and stays where it is when you let go. Change MAX_SPEED to taste, and DEADZONE if it drifts with the stick at rest.
  • Without an MPU-6050 the stick's button does nothing. With one, a click switches to tilt mode, where the servos copy the board's tilt: hold it like a remote.
  • Many joystick modules are labelled +5V. Power them from 3V3 anyway: the stick is just two potentiometers, and at 5 V their outputs could reach 5 V.
  • On the C3 the two axes use GPIO 0 and 1, the only free ADC pins once I2C and the servos are wired.

Not tested yet

  • Not tested on hardware yet. Compiles for every board listed with Arduino-ESP32 core 3.3.12.

Community builds

Loading build reports from people who made this.

Built it? Send a build report with your board and what the serial port printed. Working reports become the "Tested on this board" entries above.