Community project

Servo Angle Controller

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
Loading diagram…

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