Community project

Servo Angle Controller

Владимир

Published August 14, 2026

Arduino
Photo of Servo Angle ControllerGenerated with AI

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

Interactive · read-only
Wiring diagram for Servo Angle Controller

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
SSD1306 OLED0.96 inch, 128x6410.96 inch 128x64 OLED display with I2C interface
SG90 ServoSG90 or compatible1Micro servo motor (SG90)
Push ButtonPlus1Momentary push button switch
Push ButtonMinus1Momentary push button switch
Push ButtonSelect1Momentary push button switch

Assembly

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

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

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

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

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

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

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

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

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

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

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

Pin assignments

Board wiring reference
PinConnectionType
5Voled_1 VCCpower
GNDoled_1 GNDground
GPIO 18oled_1 SDAi2c
GPIO 19oled_1 SCLi2c
EXTservo_1 VCCSeparate regulated 5 V servo supply positivepower
GNDservo_1 GNDground
GPIO 9servo_1 SIGNALpwm
GNDbutton_plus GNDground
GPIO 2button_plus SIGNALdigital
GNDbutton_minus GNDground
GPIO 3button_minus SIGNALdigital
GNDbutton_select GNDground
GPIO 4button_select SIGNALdigital

Firmware

Arduino
main.cppDeploy to device
#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();
  }
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

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