Community project

Servo Angle Controller

Arduino
Photo of Servo Angle Controller
Generated with AI

Владимир

Published August 14, 2026

This project builds a servo angle controller that lets you precisely adjust a servo motor's position using three push buttons and real-time feedback on an OLED display. The Arduino Uno reads button inputs with debouncing, calculates the desired servo angle, and displays the current angle on a 128x64 pixel screen while driving the SG90 servo to the selected position.

The guide includes a complete wiring diagram showing how to connect the OLED display via I2C, the servo to PWM pin 9, and the three control buttons to digital pins. You'll get a full parts list, step-by-step assembly instructions, and ready-to-upload firmware with button debouncing and angle stepping logic built in.

Wiring diagram

Wiring diagram for Servo Angle Controller

Gather all the parts

QtyComponent
1

SSD1306 OLED

0.96 inch, 128x64

0.96 inch 128x64 OLED display with I2C interface

1

SG90 Servo

SG90 or compatible

Micro servo motor (SG90)

1

Push Button

Plus

Momentary push button switch

1

Push Button

Minus

Momentary push button switch

1

Push Button

Select

Momentary push button switch

Assemble it in 5 steps

1. Подготовьте общее подключение питания

Подключите Nano к USB компьютера. Для сервопривода используйте отдельный стабильный источник 5 V, способный отдать не менее 1 A. Соедините его минусовой вывод с любым GND на Nano — без общего провода управляющий сигнал не будет понятен сервоприводу.

  • Красный провод большинства SG90 — это +5 V, а коричневый или чёрный — минус.
  • Не подключайте красный провод сервопривода к выводу 5V Nano от USB: при повороте мотор может потребовать большой ток, из-за чего Nano будет перезапускаться или порт USB может отключиться.

2. Подключите маленький экран

На OLED соедините VCC с 5V Nano (питание), GND с GND Nano (земля), SDA с A4 Nano (данные) и SCL с A5 Nano (синхронизация).

  • На некоторых платах Nano выводы A4 и A5 также подписаны SDA и SCL.
  • Убедитесь, что VCC и GND не перепутаны: неверно подключённое питание может повредить экран.

3. Подключите сервопривод

Соедините оранжевый или жёлтый провод сервопривода с D9 Nano (сигнал), красный — с плюсом отдельного источника 5 V (питание), коричневый или чёрный — с минусом этого источника и с GND Nano (земля).

  • Перед первым включением снимите рычаг с вала, если механизм может упереться в ограничитель.
  • Не допускайте заклинивания рычага: зажатый сервопривод быстро потребляет большой ток и может перегреться.

4. Подключите три кнопки

У каждой кнопки используйте две противоположные ножки. Для кнопки «плюс» соедините одну ножку с D2 Nano (сигнал), вторую с GND (земля). Для «минус» соедините одну ножку с D3 (сигнал), вторую с GND. Для «выбор» соедините одну ножку с D4 (сигнал), вторую с GND. Дополнительные резисторы не нужны — плата включает их внутри себя.

  • У четырёхконтактной тактовой кнопки две ножки на одной стороне уже соединены между собой; берите ножки с разных сторон кнопки.
  • Если кнопка срабатывает постоянно, поверните её на макетной плате на 90 градусов или переставьте провод на ножку с противоположной стороны.

5. Проверьте и включите устройство

Ещё раз проверьте общий GND Nano и источника сервопривода. После включения экран на 2 секунды покажет «Squad - service», затем угол 90 градусов. Кнопками плюс и минус выберите угол; строка будет мигать, пока вы не нажмёте «выбор». Удерживайте плюс и минус одновременно одну секунду, чтобы переключать шаг 5° и 1°.

  • Строка Pulse показывает длительность управляющего импульса в микросекундах, рассчитанную для выбранного угла.
  • Перед включением убедитесь, что вал сервопривода может свободно повернуться в сторону начального положения 90 градусов.

Review all connections

1. Connections between "oled_1" and "Arduino"

Functionoled_1Arduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

2. Connections between "servo_1" and "Arduino"

Functionservo_1Arduino
powerVCC → Separate regulated 5 V servo supply positiveEXT
groundGNDGND
pwmSIGNALGPIO 9

3. Connections between "button_plus" and "Arduino"

Functionbutton_plusArduino
groundGNDGND
digitalSIGNALGPIO 2

4. Connections between "button_minus" and "Arduino"

Functionbutton_minusArduino
groundGNDGND
digitalSIGNALGPIO 3

5. Connections between "button_select" and "Arduino"

Functionbutton_selectArduino
groundGNDGND
digitalSIGNALGPIO 4

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Servo.h>

struct Button {
  uint8_t pin;
  bool stableState;
  bool lastReading;
  unsigned long changedMs;
};


// Forward declarations
bool pressed(Button &button);
int pulseForAngle(int angle);
void drawScreen();
void showSplash();

const uint8_t SERVO_PIN = 9;
const uint8_t PLUS_BUTTON_PIN = 2;
const uint8_t MINUS_BUTTON_PIN = 3;
const uint8_t SELECT_BUTTON_PIN = 4;
const uint8_t OLED_SDA_PIN = A4;
const uint8_t OLED_SCL_PIN = A5;

const unsigned long DEBOUNCE_MS = 35;
const unsigned long BOTH_HOLD_MS = 1000;
const int MIN_ANGLE = 0;
const int MAX_ANGLE = 180;
const int SERVO_MIN_US = 544;
const int SERVO_MAX_US = 2400;

// Full-buffer mode is required by this OLED driver; the screen is sent only when it changes.
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE);
Servo servo;

Button plusButton = {PLUS_BUTTON_PIN, HIGH, HIGH, 0};
Button minusButton = {MINUS_BUTTON_PIN, HIGH, HIGH, 0};
Button selectButton = {SELECT_BUTTON_PIN, HIGH, HIGH, 0};

int selectedAngle = 90;
int appliedAngle = 90;
int angleStep = 5;
unsigned long bothPressedSinceMs = 0;
bool stepChangedForCurrentHold = false;

bool pressed(Button &button) {
  bool reading = digitalRead(button.pin);
  bool pressedEvent = false;

  if (reading != button.lastReading) {
    button.changedMs = millis();
    button.lastReading = reading;
  }

  if (millis() - button.changedMs > DEBOUNCE_MS && reading != button.stableState) {
    button.stableState = reading;
    if (button.stableState == LOW) {
      pressedEvent = true;
    }
  }
  return pressedEvent;
}

int pulseForAngle(int angle) {
  return map(angle, MIN_ANGLE, MAX_ANGLE, SERVO_MIN_US, SERVO_MAX_US);
}

void drawScreen() {
  const bool pending = selectedAngle != appliedAngle;
  char pulseText[8];
  char angleText[4];

  display.clearBuffer();
  display.setFontMode(1);
  display.setDrawColor(1);
  display.setFont(u8g2_font_unifont_t_cyrillic);
  display.drawUTF8(0, 12, "УГОЛ_СЕРВО...");
  display.drawHLine(0, 15, 128);

  snprintf(angleText, sizeof(angleText), "%d", selectedAngle);
  display.setFont(u8g2_font_logisoso20_tn);
  display.drawStr(0, 37, angleText);

  display.setFont(u8g2_font_unifont_t_cyrillic);
  // Short labels fit beside the large angle number without clipping.
  display.drawUTF8(52, 36, pending ? "ОЖИДАНИЕ" : "ВЫБРАНО");

  snprintf(pulseText, sizeof(pulseText), "%d", pulseForAngle(selectedAngle));
  display.drawUTF8(0, 51, "ИМПУЛЬС");
  display.drawStr(61, 51, pulseText);
  display.drawUTF8(96, 51, "МКС");

  display.drawUTF8(0, 64, angleStep == 1 ? "ШАГ  1 ГРАДУС" : "ШАГ  5 ГРАДУСОВ");

  display.sendBuffer();
}

void showSplash() {
  const char *splashText = "Squad - service";

  display.clearBuffer();
  // This is the largest bold Latin font that keeps the complete text on one 128-pixel line.
  display.setFont(u8g2_font_helvB12_tf);
  const int textX = (128 - display.getStrWidth(splashText)) / 2;
  display.drawStr(textX, 38, splashText);
  display.sendBuffer();
  delay(2000);
}

void setup() {
  pinMode(PLUS_BUTTON_PIN, INPUT_PULLUP);
  pinMode(MINUS_BUTTON_PIN, INPUT_PULLUP);
  pinMode(SELECT_BUTTON_PIN, INPUT_PULLUP);

  Wire.begin();
  display.begin();
  display.enableUTF8Print();
  showSplash();

  servo.attach(SERVO_PIN, SERVO_MIN_US, SERVO_MAX_US);
  servo.write(appliedAngle);
  drawScreen();
}

void loop() {
  bool redraw = false;

  if (pressed(plusButton)) {
    selectedAngle = min(MAX_ANGLE, selectedAngle + angleStep);
    redraw = true;
  }
  if (pressed(minusButton)) {
    selectedAngle = max(MIN_ANGLE, selectedAngle - angleStep);
    redraw = true;
  }
  if (pressed(selectButton)) {
    appliedAngle = selectedAngle;
    servo.write(appliedAngle);
    redraw = true;
  }

  const bool bothHeld = digitalRead(PLUS_BUTTON_PIN) == LOW && digitalRead(MINUS_BUTTON_PIN) == LOW;
  if (bothHeld) {
    if (bothPressedSinceMs == 0) {
      bothPressedSinceMs = millis();
    }
    if (!stepChangedForCurrentHold && millis() - bothPressedSinceMs >= BOTH_HOLD_MS) {
      angleStep = angleStep == 5 ? 1 : 5;
      stepChangedForCurrentHold = true;
      redraw = true;
    }
  } else {
    bothPressedSinceMs = 0;
    stepChangedForCurrentHold = false;
  }

  if (redraw) {
    drawScreen();
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik