Arduino Joystick Servo Control with OLED Display (Tutorial)

This post contains affiliate links, marked with *. As an Amazon Associate I earn from qualifying purchases, at no extra cost to you. It helps keep the tutorials free.

In the video below, I build the circuit in two stages: first the joystick moves the two servos without a display, then I add the OLED. The written steps cover the finished version with the display.

The video loads from YouTube (Google), which may set cookies. Privacy policy

Quick summary

You will build a two-axis servo controller on an Arduino Uno: a joystick moves two SG90 servos, and an SSD1306 OLED shows the angle sent to each servo. The project covers analog input, servo control and an OLED on the I2C bus, which needs only two signal wires. The same circuit is the base for a pan-tilt camera mount or a small robot arm.

What you will learn

  • How the joystick module provides two analog axes (X and Y).
  • How the Arduino reads the joystick values and maps them to servo angles with the map() function.
  • How to wire two SG90 servos and an SSD1306 OLED to an Arduino Uno.
  • How the OLED shows the angle sent to each servo while you move the joystick.
  • How to power the servos from an external 5 V supply with a common ground.
  • How to stop jitter (small, unwanted servo movements) at center with a dead zone: a range around the center value in which the code ignores small joystick changes.
  • How to reverse an axis or limit the servo range.

How the circuit works

Four parts form the control loop:

  • Joystick module: It contains two potentiometers (variable resistors), VRx for the X-axis and VRy for the Y-axis. Each one outputs a voltage between 0 and 5 V, which the Arduino reads as a value from 0 to 1023.
  • Arduino Uno: It runs the sketch (an Arduino program). The sketch reads both values, converts them to angles from 0 to 180 degrees with map(), sends the angles to the servos and updates the OLED.
  • Two servo motors: Each servo receives a PWM (pulse-width modulation) signal from its own Arduino pin. The signal is a pulse every 20 ms; its width, about 0.5 to 2.4 ms in the Arduino Servo library, sets the angle. The X-axis controls the servo on D9, the Y-axis the servo on D10.
  • SSD1306 OLED display: The OLED is a 128×64 pixel screen. It talks to the Arduino over I2C: SDA carries the data, SCL the clock signal.

One pass of the loop takes 50 ms plus the time to refresh the OLED, so the servos follow the joystick without a visible delay. New to servos? The Arduino Servo Control Guide explains the basics.

Components needed

ComponentQuantityPurposeNotes
Arduino Uno (or Nano)1Main microcontroller boardAny Arduino with the ATmega328P microcontroller chip works
Joystick module (XY + SW)1Two-axis analog inputProvides VRx, VRy and an optional push button (SW)
Servo motor (SG90 or MG90S)2Actuators for the X and Y axesSmall hobby servos rated for 4.8 to 6 V
OLED display (SSD1306, I2C)1Shows the angle sent to each servo128×64 pixels, 3.3 to 5 V
Breadboard + jumper wiresas neededWiringMale-to-male and male-to-female jumper wires
External 5 V supply1Servo powerAt least 2 A for two servos

Wiring diagram

Connect the components as listed in the table. If you are new to Arduino wiring, read Arduino Programming Basics first.

Component pinArduino pinNotes
Joystick VRxA0X-axis analog output
Joystick VRyA1Y-axis analog output
Joystick SWD2 (optional)Push button, not used in this sketch
Servo 1 signalD9PWM output for the X-axis servo
Servo 2 signalD10PWM output for the Y-axis servo
OLED SDAA4I2C data line
OLED SCLA5I2C clock line
Servo +V (both)External 5 VDo not power servos from the Arduino 5 V pin
Servo GND (both)Common GNDConnect external supply GND to Arduino GND

Important wiring notes:

  • External 5 V supply for the servos: An SG90 draws about 100 mA when it moves freely and up to about 650 mA when it stalls. A stalled servo is blocked and cannot reach its target. Two stalled servos draw up to 1.3 A, while a USB port supplies 500 mA. The result is voltage drops, servo jitter or board resets. Use an external 5 V supply rated for at least 2 A.
  • Common ground: The GND of the external supply, the Arduino GND and all component GND pins must be connected together. Without a common ground, the servos have no reference for the signal voltage and respond erratically or not at all.
  • I2C pull-ups: Most SSD1306 modules have pull-up resistors on SDA and SCL. A pull-up resistor holds the line at a defined voltage when no device pulls it low. If your display has none, add 4.7 kΩ pull-ups to 3.3 V or 5 V.
Wiring diagram: Arduino Uno, joystick on A0 and A1, two SG90 servos on D9 and D10, SSD1306 OLED on A4 and A5, external 5 V supply with common ground

Programming the Arduino

Before uploading, install these libraries in the Arduino IDE (the program you use to write and upload sketches): Servo (built in), Adafruit GFX and Adafruit SSD1306. Open Sketch > Include Library > Manage Libraries, search for Adafruit SSD1306 and click Install. Accept the prompt to install Adafruit GFX as well.

Full code

Paste the code into a new sketch in the Arduino IDE and upload it to the Uno.

#include <Servo.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// OLED setup
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Servo setup
Servo servoX;
Servo servoY;
int joyX = A0;
int joyY = A1;
int servoXPin = 9;
int servoYPin = 10;
void setup() {
  servoX.attach(servoXPin);
  servoY.attach(servoYPin);
  // OLED init
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    for(;;); // Halt if the display buffer cannot be allocated
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
}
void loop() {
  int xVal = analogRead(joyX);
  int yVal = analogRead(joyY);
  int angleX = map(xVal, 0, 1023, 0, 180);
  int angleY = map(yVal, 0, 1023, 0, 180);
  servoX.write(angleX);
  servoY.write(angleY);
  // OLED output
  display.clearDisplay();
  display.setCursor(0,0);
  display.print("Servo X: "); display.println(angleX);
  display.print("Servo Y: "); display.println(angleY);
  display.display();
  delay(50); // at most 20 updates per second
}

Code explanation

Libraries and setup

The sketch uses three libraries. Servo.h provides the Servo class, which generates the control signal for hobby servos. Adafruit_GFX.h is the graphics library for text, shapes and pixels. Adafruit_SSD1306.h is the driver for SSD1306 OLEDs over I2C.

The line Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1) creates the display object for a 128×64 screen. The argument &Wire selects the Uno’s I2C bus, and -1 means no reset pin is connected. In setup(), attach() links each Servo object to its pin. The call display.begin(SSD1306_SWITCHCAPVCC, 0x3C) starts the display at I2C address 0x3C, the default for most 128×64 SSD1306 modules. The flag SSD1306_SWITCHCAPVCC tells the display to generate its high panel voltage internally, so it needs no extra supply. The call display.begin() also reserves 1 KB of the Uno’s 2 KB RAM for the image buffer, a copy of the screen held in the Arduino’s memory. If that memory is not free, begin() returns false and the line for(;;); stops the program in an endless loop. The call does not check whether a display answers on the I2C bus.

Joystick analog reading

The calls analogRead(joyX) and analogRead(joyY) read the voltage on A0 and A1. Each joystick potentiometer outputs 0 V at one end of its travel and 5 V at the other. The Arduino’s ADC (analog-to-digital converter) turns this voltage into a number from 0 to 1023. At center position, you will typically read values around 500 to 520. With my joystick at rest, the OLED showed 88 degrees on X and 91 on Y, which corresponds to raw values of about 503 and 520.

Angle mapping with map()

The call map(xVal, 0, 1023, 0, 180) scales the raw joystick value (0 to 1023) linearly to a servo angle (0 to 180 degrees). A raw value of 0 gives 0 degrees, 1023 gives 180 degrees, and the center value of about 510 gives about 90 degrees. Which joystick direction gives 0 depends on how the module is mounted; Step 4 shows how to reverse it. The Y-axis uses the same mapping.

Servo output

The calls servoX.write(angleX) and servoY.write(angleY) send the calculated angle to each servo. The Servo library generates the PWM signal.

OLED display update

On every pass of loop(), display.clearDisplay() erases the previous content, display.setCursor(0,0) moves the text cursor to the top-left corner, and print() writes both angles. The call display.display() sends the image buffer to the OLED; print() only changes this buffer. The display shows the angle sent to each servo, which helps during calibration.

Why delay(50) is used

The 50 ms delay limits the loop to at most 20 updates per second. The Servo library sends its pulse every 20 ms regardless of the delay, so the delay does not change the PWM signal. It reduces how often small joystick noise reaches the servos. If the servos still jitter at rest, add the dead zone from Step 3.

How to test and calibrate the setup

Step 1: Test both axes

Move the joystick left and right: the servo on D9 turns from 0 to 180 degrees and the Servo X value on the OLED changes. Move it up and down: the servo on D10 turns and the Servo Y value changes.

Step 2: Check the center position

With the joystick at center, both values on the OLED read about 90 degrees. If the display stays blank, see the Troubleshooting section.

Step 3: Add a dead zone for center stability

If the servos jitter while the joystick rests at center, add a dead zone. After the two map() lines, add one line per axis:

if (xVal > 480 && xVal < 540) angleX = 90;
if (yVal > 480 && yVal < 540) angleY = 90;

While the joystick rests near center, both servos hold 90 degrees and ignore small analog noise. The rest values of my module, about 503 and 520, both fall inside this window.

Step 4: Reverse an axis if needed

If a servo moves opposite to the joystick, reverse the output range for that axis: change map(xVal, 0, 1023, 0, 180) to map(xVal, 0, 1023, 180, 0).

Step 5: Limit the servo angles for your mechanics

If your mechanics cannot use the full 0 to 180 degree range, change the output range. A pan-tilt mount that allows 30 to 150 degrees, for example, needs map(xVal, 0, 1023, 30, 150). The servo then does not push against the mechanical stops, which can damage the gears over time. In my build, the two servos lay loose on the table with only their horns attached, so I could use the full range.

Troubleshooting

OLED stays blank

Check that SDA is on A4 and SCL on A5, and that the Adafruit SSD1306 and Adafruit GFX libraries are installed. Then check that the I2C address in display.begin(SSD1306_SWITCHCAPVCC, 0x3C) matches your display. Most modules use 0x3C, and the 0.96-inch module in my build did too. If the display has never worked, change the address in that line to 0x3D or run an I2C scanner sketch, which lists the addresses of all connected I2C devices. If it worked before and stopped, look for a loose SDA, SCL or power wire.

Servos jitter at center position

Jitter at center has two usual causes: weak power (servos on the Arduino 5 V pin) or analog noise from the joystick. Check the power first (see the wiring notes), then add the dead zone from Step 3. A 470 µF electrolytic capacitor (rated 10 V or more) across the servo supply lines, close to the servos, smooths short voltage dips. Connect the side marked with the minus stripe to GND.

Servos move in the wrong direction

Reverse the output range for that axis, as shown in Step 4.

One servo does not respond

Check the signal wire of that servo and the pin in the code (D9 or D10). Test the servo on its own with the Sweep example (File > Examples > Servo > Sweep), which turns the servo on pin 9 back and forth, to rule out a defective servo. Check that the servo GND is connected to the common ground.

Display shows random pixels or shifted text

Check the controller chip and the size of your module. Many 1.3-inch modules use the SH1106 chip, which needs a different library, and a 128×32 module needs SCREEN_HEIGHT 32. If you changed the code, check that display.display() is still called after printing.

System resets when both servos move

The usual cause is insufficient power: two servos that start or stall at the same time draw more than a USB port supplies. Power the servos from an external 5 V supply as described in the wiring notes, and connect only its ground to the Arduino.

Joystick values feel noisy

Some joystick modules give noisy analog readings. To smooth them, read each axis several times (for example four) and use the average, or add the dead zone from Step 3. Keep the joystick wires short and away from the servo wires.

Frequently asked questions

Can I use a different Arduino board?

Yes. This project works with any Arduino that has two analog inputs and two free digital pins. The Servo library creates its signal with a timer on any digital pin, so it does not need the hardware PWM pins marked with ~ on the board. Any 5 V Arduino with the same pins works without changes, for example the Nano. The Mega and Leonardo work too, but their I2C pins are not A4 and A5 (Mega: 20 and 21, Leonardo: 2 and 3), so move the SDA and SCL wires accordingly.

How many servos can I control with one joystick?

One joystick has two axes, so it controls two servos. The Uno has six analog inputs (A0 to A5), but A4 and A5 carry the I2C signals for the OLED, so two joysticks (four servos) fit next to the display. For more servos, use the SW button to switch the joystick between servo pairs, or add a PCA9685 driver, which controls up to 16 servos over I2C.

Do I need the OLED for this project to work?

No. The servos follow the joystick without the display, because display.begin() does not check whether a display is connected. The OLED makes testing and calibration easier, since it shows the angle sent to each servo.

Can I use MG996R servos instead of SG90?

Yes, but an MG996R draws far more current: TowerPro lists a stall current of 2.5 A at 6 V. Size the external supply for the stall current of both servos. The code stays the same, because both servo types use the same control signal.

Resources and next steps

Related OmArTronics tutorials

Suggested next upgrades

  • Use the joystick SW pin to save servo positions or switch between control modes.
  • Replace the joystick with a smartphone app that talks to the Arduino over an HC-05 Bluetooth module.
  • Build a two-axis camera mount and control it with this circuit.
  • Add more servos and joysticks for a robot arm with more joints.
  • Store servo positions in EEPROM, the Arduino’s memory that keeps data without power, and replay them as a motion sequence.

Leave a Comment