Если у вас обычный позиционный SG90 (0–180°), см. раздел «Альтернатива для позиционного SG90» в конце. В основном уроке используем непрерывный (360°) SG90/FS90R: у него «скорость/направление» задаются шириной импульса, 1500 мкс = стоп.
Будем использовать детектор одиночного/двойного клика, «мёртвую зону» джойстика и плавное вычисление скважности (ширины импульса) для сервомотора.
Серво (SG90 360°):
+
и -
перемычки установленны в соответствии с линией подключения 5V+
и -
.Джойстик HW‑504:
INPUT_PULLUP
)⚠️ Питание: не запитывайте серво от 5V Arduino — просадки и перезагрузки гарантированы. Внешний 5В‑модуль питания обязателен. Подключите его отдельным Общая земля между БП и Arduino обязательна.
Для сервопривода непрерывного вращения:
writeMicroseconds(1500)
— СТОП< 1500
— вращение в одну сторону (скорость ниже → ближе к 1500)> 1500
— в другую (чем дальше от 1500, тем быстрее)Мы используем X‑ось джойстика: от середины (≈512) считаем модуль отклонения и пересчитываем его в «дельту» импульса (например, от ±60 до ±380 мкс). Это даёт хорошую «минималку» и полный ход.
/*
* SG90 (360°) + HW-504 joystick: single/double click, CW/CCW, speed by X
* Пины: SERVO=D9, JOY_X=A0, JOY_SW=D2 (кнопка на GND, INPUT_PULLUP)
* Питание серво — ОТДЕЛЬНЫЙ 5V БП! GND общий с Arduino.
*/
#include <Servo.h>
// ===== ПИНЫ =====
const uint8_t PIN_SERVO = 9;
const uint8_t PIN_JOY_X = A0;
const uint8_t PIN_JOY_SW = 2;
// ===== КАЛИБРОВКА / НАСТРОЙКИ =====
const bool INVERT_DIR = false; // если CW/CCW «перепутаны» — поменяйте на true
const int PULSE_STOP = 1500; // микросекунды, стоп
const int MIN_DELTA_US = 60; // минимальная «скорость» (дельта от 1500)
const int MAX_DELTA_US = 380; // максимальная «скорость» (дельта от 1500)
const int JOY_CENTER = 512; // центр джойстика
const int JOY_DEAD = 50; // мёртвая зона вокруг центра
const int JOY_MAX = 1023;
// Детектор кликов
const unsigned long DEBOUNCE_MS = 30;
const unsigned long DC_WINDOW_MS = 350; // окно для двойного клика
// ===== СОСТОЯНИЕ =====
enum RunState { STOPPED, RUN_CW, RUN_CCW };
RunState state = STOPPED;
Servo sv;
// для кликов
bool btnStable = HIGH, btnPrevStable = HIGH;
unsigned long lastBounce = 0;
int clickCount = 0;
unsigned long firstClickAt = 0;
// сглаживание значения X
int smoothX = JOY_CENTER;
// ===== УТИЛИТЫ =====
int readJoyX() {
int raw = analogRead(PIN_JOY_X);
// простое экспоненциальное сглаживание
smoothX = (smoothX * 3 + raw) / 4;
return smoothX;
}
// возвращает дельту микросекунд [MIN_DELTA_US..MAX_DELTA_US] в зависимости от отклонения
int computeDeltaFromJoy() {
int x = readJoyX();
int dx = abs(x - JOY_CENTER) - JOY_DEAD;
if (dx < 0) dx = 0;
int span = (JOY_MAX / 2) - JOY_DEAD;
if (span < 1) span = 1;
// нормируем [0..1]
float k = (float)dx / (float)span;
if (k > 1.0f) k = 1.0f;
// дельта микросекунд
int delta = MIN_DELTA_US + (int)((MAX_DELTA_US - MIN_DELTA_US) * k);
return delta;
}
void applyOutput() {
int delta = computeDeltaFromJoy(); // всегда >= MIN_DELTA_US
int pulse = PULSE_STOP;
if (state == RUN_CW) {
pulse = PULSE_STOP + (INVERT_DIR ? +delta : -delta);
} else if (state == RUN_CCW) {
pulse = PULSE_STOP + (INVERT_DIR ? -delta : +delta);
} else {
pulse = PULSE_STOP;
}
sv.writeMicroseconds(pulse);
}
// обработчики событий
void startCW() { state = RUN_CW; }
void startCCW() { state = RUN_CCW; }
void stopRun() { state = STOPPED; sv.writeMicroseconds(PULSE_STOP); }
// детектор одиночного/двойного клика
void handleClicksIfReady() {
if (clickCount > 0 && (millis() - firstClickAt) > DC_WINDOW_MS) {
if (clickCount == 1) {
// одиночный клик: если стояли — старт CW; если крутимся — стоп
if (state == STOPPED) startCW(); else stopRun();
} else {
// двойной клик: запуск CCW
startCCW();
}
clickCount = 0;
}
}
void setup() {
pinMode(PIN_JOY_SW, INPUT_PULLUP);
sv.attach(PIN_SERVO);
sv.writeMicroseconds(PULSE_STOP);
Serial.begin(115200);
delay(800);
Serial.println(F("SG90 360 + HW-504: single/double click, speed by X"));
Serial.println(F("Single: start CW / stop; Double: start CCW; X: speed"));
}
void loop() {
// === кнопка с антидребезгом ===
bool reading = digitalRead(PIN_JOY_SW);
if (reading != btnStable && (millis() - lastBounce) > DEBOUNCE_MS) {
lastBounce = millis();
btnStable = reading;
// событие "отпускание" (кнопка нажимает в LOW)
if (btnStable == HIGH && btnPrevStable == LOW) {
if (clickCount == 0) {
firstClickAt = millis();
}
clickCount++;
}
btnPrevStable = btnStable;
}
handleClicksIfReady();
// === управление сервом ===
applyOutput();
// (опционально) отладка раз в ~100 мс
static unsigned long lastPrint = 0;
if (millis() - lastPrint > 150) {
lastPrint = millis();
Serial.print(F("State="));
Serial.print(state == STOPPED ? F("STOP") : (state == RUN_CW ? F("CW") : F("CCW")));
Serial.print(F(" X=")); Serial.print(smoothX);
Serial.println();
}
}
STOPPED
, RUN_CW
, RUN_CCW
. Клики переводят между ними:
STOPPED→RUN_CW
, RUN_*→STOPPED
→ RUN_CCW
true/false
.Перед компиляцией установите/подключите:
Если у вас обычный SG90, можно имитировать «вращение» непрерывной подачи мелких приращений угла. Клик — задаёт направление, джойстик — задаёт скорость приращений. Это будет не скорость вращения, а скорость изменения угла; при достижении 0/180° придётся «перекидываться» обратно (или использовать шестерню/механику). Код‑набросок:
// ВМЕСТО writeMicroseconds() используйте углы 0..180
// Идея: angle += dir * step; step зависит от отклонения джойстика
#include <Servo.h>
Servo sv;
int angle = 90; // старт из середины
int dir = 0; // -1, 0, +1
unsigned long lastStep=0;
void loop() {
// ... обработка кликов: dir = +1 (CW), 0 (stop), -1 (CCW)
int delta = computeDeltaFromJoy(); // 60..380
int stepDelay = map(delta, 60, 380, 40, 3); // больше дельта -> быстрее шаги
if (dir != 0 && millis()-lastStep >= (unsigned)stepDelay) {
lastStep = millis();
angle += dir; // шаг 1°
if (angle >= 180) angle = 0; // «перекидываемся»
if (angle < 0) angle = 179;
sv.write(angle);
}
}