Cranes, camera mounts and robot arms are not steered by setting an angle directly. The operator pushes a stick, and the machine moves in that direction for as long as the stick is pushed, faster when it's pushed further. This is called rate control. In this lesson you build it with the joystick and a servo, and you see why it feels much more natural than having the angle follow the stick.
What you'll learn
- How to find a joystick's middle position at start-up
- How to ignore small movements with a dead zone
- What rate control is and how to program it
- How to use the joystick's built-in button
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Joystick module, KY-023 (ESP32 IoT Starter Kit)
- Servo motor (ESP32 IoT Starter Kit)
- A straw or a strip of card as a pointer
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Make it real: tape a straw to the servo horn as a pointer and draw a half circle with marks on card under it.
Wiring

| From | To |
|---|---|
| Joystick GND | GND |
| Joystick +5V | 3V3 (the ESP32 can only measure up to 3.3 V) |
| Joystick VRx | ESP32 GPIO 34 |
| Joystick VRy | Not used |
| Joystick SW | ESP32 GPIO 32 |
| Servo orange / yellow wire | ESP32 GPIO 13 |
| Servo red wire | 5V |
| Servo brown / black wire | GND |
The sketch
Install the ESP32Servo library (by Kevin Harrington) in the Arduino IDE. Leave the joystick in the middle while the ESP32 starts: it measures the middle position then.
#include <ESP32Servo.h>
const int X_PIN = 34;
const int SW_PIN = 32;
const int SERVO_PIN = 13;
const int DEAD_ZONE = 250; // ignore small movements around the middle
const float MAX_SPEED = 3.0; // degrees per step at full push
Servo pan;
int middle = 2048;
float angle = 90;
void setup() {
Serial.begin(115200);
pinMode(SW_PIN, INPUT_PULLUP);
pan.setPeriodHertz(50);
pan.attach(SERVO_PIN, 500, 2400);
pan.write(90);
long total = 0; // measure the middle position
for (int i = 0; i < 32; i++) {
total += analogRead(X_PIN);
delay(5);
}
middle = total / 32;
Serial.print("Middle position: ");
Serial.println(middle);
}
void loop() {
int push = analogRead(X_PIN) - middle; // negative = one way, positive = the other
if (abs(push) > DEAD_ZONE) {
float strength = (abs(push) - DEAD_ZONE) / (2048.0 - DEAD_ZONE); // 0 ... 1
float speed = constrain(strength, 0.0, 1.0) * MAX_SPEED;
angle += (push > 0) ? speed : -speed;
angle = constrain(angle, 0, 180);
pan.write((int)angle);
}
if (digitalRead(SW_PIN) == LOW) { // click: back to the middle
angle = 90;
pan.write(90);
}
delay(20); // 50 steps per second
}
How it works
Every joystick is slightly different, and its middle reading is rarely exactly 2048. The sketch measures it 32 times at start-up and uses the average as "middle". Anything within 250 of the middle counts as "not pushed": that dead zone stops the servo from creeping when you let go of the stick.
With rate control, the stick sets the speed, not the angle. Fifty times per second the sketch adds a small amount to the angle: nothing in the dead zone, up to 3 degrees at full push. Let go and the servo stays where it is. That's why you can aim precisely with small pushes and still turn quickly with a big one. The angle is kept in a float so that small steps such as 0.4 degrees add up instead of being lost.
Try this next
- Use VRy and a second servo for a pan-and-tilt mount.
- Make the speed curve gentler for small pushes: use
strength * strength. - Print the angle in the Serial Monitor and save favourite positions with the button.
Troubleshooting
-
The servo creeps by itself: leave the stick in the middle while the ESP32 starts, or raise
DEAD_ZONE. -
It turns the wrong way: swap
speed : -speedto-speed : speed. - The click does nothing: check SW on GPIO 32.
- The ESP32 restarts when the servo moves: servos draw bursts of current. Use a good USB port or a powered hub, and check that the servo's red wire is on 5V.