Коротко: пять светодиодов в ряд и джойстик. Наклон влево или вправо переносит огонёк на соседний светодиод, нажатие на джойстик заставляет его мигать, повторное нажатие — снова гореть ровно.

Зачем
Светофор только выводил сигналы и ничего не знал о внешнем мире. Здесь плата впервые реагирует на действия человека: читает положение джойстика и нажатие кнопки.
Задача выглядит простой, но в ней спрятана главная проблема первого урока — delay(). Пока плата ждёт, чтобы мигнуть светодиодом, она не видит джойстик. Значит, мигать придётся по-другому.
Что понадобилось
- плата Arduino;
- модуль джойстика;
- пять светодиодов одного цвета и пять резисторов на 220 Ом;
- макетная плата и провода Dupont.
Светодиоды подключаю так же, как в светофоре: пин → резистор → светодиод → GND. Джойстик — пятью проводами:
| Что | Куда |
|---|---|
| Светодиоды 1–5 | пины 2, 3, 4, 5, 6 |
Джойстик GND |
GND |
Джойстик +5V |
5V |
Джойстик VRx |
A0 |
Джойстик VRy |
не нужен |
Джойстик SW |
пин 7 |


Как устроен джойстик
Внутри модуля два потенциометра — по одному на каждую ось — и кнопка, которая срабатывает при нажатии на ручку.
Каждый потенциометр включён делителем напряжения, как в уроке о мультиметре. В центре на выходе VRx примерно 2,5 В, при наклоне до упора — около 0 или 5 В. Это легко проверить мультиметром.
Arduino читает это напряжение функцией analogRead() и возвращает число от 0 до 1023. Прежде чем писать игру, стоит посмотреть, какие числа выдаёт именно ваш джойстик. Для этого достаточно короткого скетча, который печатает значение в монитор порта:
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println(analogRead(A0));
delay(100);
}
В центре получится число около 512, при наклоне — близко к 0 или к 1023. Какое направление даёт 0, зависит от того, как повёрнут модуль. У меня наклон вправо даёт числа около 0 — это пригодится ниже.
Как собирал
Сначала целиком скетч, потом разберу его по частям:
const int LED_PINS[] = {2, 3, 4, 5, 6};
const int LED_COUNT = sizeof(LED_PINS) / sizeof(LED_PINS[0]);
const int JOY_X = A0;
const int JOY_BUTTON = 7;
const int LOW_THRESHOLD = 300; // наклон в сторону 0
const int HIGH_THRESHOLD = 700; // наклон в сторону 1023
const unsigned long BLINK_INTERVAL = 300;
const unsigned long DEBOUNCE_DELAY = 50;
int current = 0; // какой светодиод выбран
bool blinking = false; // мигает или горит ровно
bool stickCentered = true; // джойстик вернулся в центр
bool ledOn = true;
unsigned long lastBlink = 0;
int lastButtonReading = HIGH;
int buttonState = HIGH;
unsigned long lastDebounce = 0;
void setup() {
for (int i = 0; i < LED_COUNT; i++) {
pinMode(LED_PINS[i], OUTPUT);
}
pinMode(JOY_BUTTON, INPUT_PULLUP);
}
void loop() {
readStick();
readButton();
updateLeds();
}
void readStick() {
int x = analogRead(JOY_X);
if (x > LOW_THRESHOLD && x < HIGH_THRESHOLD) {
stickCentered = true;
return;
}
if (!stickCentered) return;
stickCentered = false;
if (x <= LOW_THRESHOLD && current < LED_COUNT - 1) current++;
if (x >= HIGH_THRESHOLD && current > 0) current--;
ledOn = true;
lastBlink = millis();
}
void readButton() {
int reading = digitalRead(JOY_BUTTON);
if (reading != lastButtonReading) {
lastDebounce = millis();
}
lastButtonReading = reading;
if (millis() - lastDebounce < DEBOUNCE_DELAY) return;
if (reading == buttonState) return;
buttonState = reading;
if (buttonState == LOW) {
blinking = !blinking;
ledOn = true;
lastBlink = millis();
}
}
void updateLeds() {
if (blinking && millis() - lastBlink >= BLINK_INTERVAL) {
ledOn = !ledOn;
lastBlink = millis();
}
for (int i = 0; i < LED_COUNT; i++) {
bool on = (i == current) && (!blinking || ledOn);
digitalWrite(LED_PINS[i], on ? HIGH : LOW);
}
}
loop() теперь состоит из трёх вызовов: прочитать джойстик, прочитать кнопку, обновить светодиоды. Ни один из них не ждёт, поэтому цикл крутится тысячи раз в секунду, и плата успевает следить за всем сразу.
Массив вместо пяти констант
В светофоре у каждого светодиода была своя константа. Здесь их пять, и они одинаковые по смыслу, поэтому лежат в массиве LED_PINS. Выбранный светодиод — это просто индекс current, а переместить огонёк значит прибавить или отнять единицу.
LED_COUNT считается из размера массива. Если добавить в ряд шестой светодиод, достаточно дописать его пин в массив — остальной код менять не придётся.
Джойстик: один наклон — один шаг
Если просто проверять «джойстик наклонён вправо», огонёк улетит в конец ряда за долю секунды: loop() успеет выполниться сотни раз, пока рука держит ручку.
Поэтому в readStick() есть флаг stickCentered. Огонёк сдвигается только в момент наклона, а следующий шаг возможен, когда джойстик вернётся в центр. Пороги LOW_THRESHOLD и HIGH_THRESHOLD (300 и 700) оставляют вокруг центра запас, чтобы небольшое дрожание ручки не считалось наклоном.
Кнопка: подтяжка и дребезг
Кнопка джойстика замыкает пин на GND. Пока она отпущена, пин ни к чему не подключён и ловит наводки — digitalRead() будет возвращать случайные значения. Режим INPUT_PULLUP включает внутри платы резистор, который подтягивает пин к 5 В. В итоге логика перевёрнута: отпущена — HIGH, нажата — LOW.
Вторая проблема — дребезг. Контакты кнопки при нажатии несколько миллисекунд стучат друг о друга, и плата видит не одно нажатие, а пачку. Режим мигания тогда переключится случайное число раз.
readButton() решает это так: принимает новое состояние кнопки, только если оно не менялось 50 мс. И реагирует на переход в LOW — момент нажатия, а не на всё время, пока кнопка зажата.
Мигание без delay()
В светофоре я бы написал delay(300) между включением и выключением. Здесь так нельзя: на эти 300 мс плата оглохнет, и наклон джойстика потеряется.
Вместо ожидания updateLeds() смотрит на часы. millis() возвращает, сколько миллисекунд прошло с включения платы. Если с последнего переключения прошло больше BLINK_INTERVAL, светодиод меняет состояние, а время запоминается. Если нет — функция ничего не делает и сразу возвращает управление.
Для бэкендера это знакомый приём: не блокировать поток ожиданием, а на каждой итерации цикла проверять, не пора ли что-то сделать.
После перемещения огонька и после нажатия кнопки ledOn сбрасывается в true. Без этого огонёк мог бы переехать на новое место в фазе «выключен» и на мгновение пропасть из виду.