Манипулятор на двух серво был моей любимой поделкой: одна серво крутит основание, вторая поднимает шею, на конце — лазерная указка. Управлял я им четырьмя кнопками: влево, вправо, вверх, вниз. Каждое нажатие — ступень, серво съезжает на два градуса и замирает. От нейтрали до цели по горизонтали было градусов тридцать, то есть пятнадцать нажатий туда и столько же обратно. Движение выходило ступенчатым, как у робота из дешёвого фильма, хотя вести серво плавно мы уже умеем — в уроке про плавное движение серво ход расписан по градусу за раз. Не хватало другого: непрерывного руля, который чувствуется пальцами, а не считается нажатиями.
Ответ на это — аналоговый джойстик, такая же ручка-стик, как на геймпаде. Внутри дешёвого модуля KY-023 сидят два потенциометра и тактовая кнопка: наклоняешь ручку в любую сторону — оба выдают непрерывные значения, а нажатие на ручку сверху — это отдельная кнопка «выбрать». Ни библиотек, ни ШИМ не нужно: только analogRead, digitalRead и голова.
План урока: сначала читаем обе оси и кнопку сырыми числами и учимся печатать их так, чтобы довольны были и монитор, и плоттер. Потом упираемся в честный тупик — дрожь в центре, — вводим калибровку центра и мёртвую зону, переводим значения в четыре направления и собираем всё в симуляторе: движок сам построит два ползунка под оси и кнопку под пин 2.
Что внутри модуля KY-023
Модуль KY-023 — платка с тремя полезными вещами. Первые две — потенциометры по 10 кОм, ровно те же детали, что мы крутили отвёрткой в уроке про потенциометр и analogRead: три ножки, крайние на питание, средняя — съёмная. Ручка джойстика механически связана с движками обоих: наклон влево-вправо двигает один движок, вверх-вниз — второй. Средние ножки выведены на контакты VRx и VRy. Ручка на пружинке: отпускаешь — сама возвращается в центр, а при вдавливании щёлкает встроенная кнопка. Стоит модуль дешевле чашки кофе, и это его главный аргумент: испортить при экспериментах не жалко.
Выводов у модуля пять: GND, +5V, VRx, VRy и SW — кнопка. Оси читаем analogRead-ом: АЦП на Uno 10-битный, диапазон 0–1023, одно чтение занимает около 100 мкс, так что обе оси плюс кнопка опрашиваются быстрее, чем за треть миллисекунды. В центре ручка даёт примерно половину диапазона — около 512, у упора — 0 или 1023. Какой край в какую сторону — зависит от распайки: у двух моих клонов направления не совпали, и это нормально.
Кнопка SW замыкает свой контакт на GND, когда ручку вдавливаешь сверху. Настраиваем пин как INPUT_PULLUP — и получаем инвертированную логику из урока про кнопку: в покое HIGH, при нажатии LOW. Внешние резисторы не нужны, а жест «нажать и удержать» сам просится на роль «подтвердить».
const int OSE_X = A0;const int OSE_Y = A1;const int KNOPKA = 2; void setup() { pinMode(KNOPKA, INPUT_PULLUP); Serial.begin(9600);} void loop() { int x = analogRead(OSE_X); int y = analogRead(OSE_Y); int knopka = digitalRead(KNOPKA); Serial.print("x="); Serial.print(x); Serial.print(" y="); Serial.print(y); Serial.print(" knopka="); Serial.println(knopka); delay(300);}Обратите внимание на формат печати: значения выходят парами имя=значение в одной строке — x=512 y=503 knopka=1. Монитору такой формат удобен, чтобы читаться глазами без напряга, а плоттер из каждой строки получает три именованных ряда — как мы разбирали в уроке про Serial Plotter. Голые числа через запятую тоже работают, но ряды в них обезличены, а с двумя осями легко перепутать, кто из них кто.
Цена у этой печати копеечная. Строка из 22 символов при 9600 бод — это около 22 мс в порту: 9600 бод — это примерно 960 символов в секунду, по миллисекунде на символ. При delay(300) порт занят около семи процентов времени — для джойстика арифметика сходится с запасом. Если поднять скорость до 115200, тот же символ будет стоить около 87 мкс, но и стандартных девяти тысяч шестисот для трёх строк в секунду хватает с избытком.
Кнопка в покое печатает 1 — это HIGH от подтяжки, при нажатии станет 0. А теперь главный сюрприз первого знакомства: поставьте ручку в центр и не трогайте. Значения не замрут на 512. У меня ось X гуляла между 505 и 519, ось Y стояла на 519 и шумела на пару значений туда-сюда. Это не брак — это жизнь, и с ней разбирается следующий раздел.
| Канал | Пин | В центре | На краях |
|---|---|---|---|
| Ось X: влево-вправо | A0 (VRx) | около 512 | 0 — до упора в одну сторону, 1023 — в другую |
| Ось Y: вверх-вниз | A1 (VRy) | около 512 | те же 0 и 1023; какая сторона какой — зависит от модуля |
| Кнопка SW | D2, INPUT_PULLUP | 1 (HIGH) | 0 (LOW), пока ручка вдавлена |
Мёртвая зона: почему курсор дрожит в центре
Первую версию я вешал на курсор: отклонение оси — смещение точки на экране в пикселях. Схема работала, но точка в центре дрожала и ползла: 506, 512, 517, 509 — каждый кадр новое значение, курсор трясся на 6–8 пикселей, будто палец лежит на ручке. Руки в этот момент были в карманах. Полчаса я гонял модуль на брак и уже искал коробку от возврата, а потом понял: я спрашиваю у дешёвого потенциометра идеально ровный центр, которого не бывает.
Причин у дрожи три, и все честные. Первая: центр каждого экземпляра свой — механический ноль ручки редко попадает в электрическую середину 512; у моего X стоял на 505, Y — на 519. Вторая: шум — дешёвые потенциометры дают разброс в 2–3 значения от замера к замеру даже на неподвижной ручке. Третья: рука дрожит физически, и это норма — поднесите палец к ручке, не касаясь, и увидите, как числа откликаются.
Лечение — из двух частей. Калибровка: при старте, пока ручку никто не трогает, читаем каждую ось двадцать раз и берём среднее. Двадцать замеров по 100 мкс — это 2 мс в setup(), а запомненный центр становится точкой отсчёта вместо бумажных 512. Мёртвая зона: если отклонение от центра меньше порога — считаем ручку стоящей в центре и ни на что не реагируем.
Порог — вещь личная. Мёртвая зона в 40 значений — моя норма для дешёвых модулей: это 80 значений из 1024, около восьми процентов диапазона, и рука такого порога не замечает. У фирменных модулей с аккуратными потенциометрами хватает 20. А для платформы с вибрирующими моторами я поднимал до 60 — иначе ось ловила дрожь корпуса.
const int KNOPKA = 2;const int MERTVAYA = 40; // порог мёртвой зоны в значениях АЦП int centerX = 512;int centerY = 512; void napravlenieTekstom(int kod) { if (kod == 1) { Serial.println("Napravlenie: levo"); } else if (kod == 2) { Serial.println("Napravlenie: pravo"); } else if (kod == 3) { Serial.println("Napravlenie: vverh"); } else if (kod == 4) { Serial.println("Napravlenie: vniz"); } else { Serial.println("Napravlenie: centr"); }} void setup() { pinMode(KNOPKA, INPUT_PULLUP); Serial.begin(9600); // калибровка: 20 замеров по 5 мс, ручку никто не трогает long sumX = 0; long sumY = 0; for (int i = 0; i < 20; i++) { sumX = sumX + analogRead(A0); sumY = sumY + analogRead(A1); delay(5); } centerX = sumX / 20; centerY = sumY / 20; Serial.print("centerX="); Serial.println(centerX); Serial.print("centerY="); Serial.println(centerY);} void loop() { int x = analogRead(A0); int y = analogRead(A1); int knopka = digitalRead(KNOPKA); int dx = x - centerX; int dy = y - centerY; if (knopka == LOW) { Serial.println("Klik: vybor"); } else if (abs(dx) > MERTVAYA || abs(dy) > MERTVAYA) { if (abs(dx) >= abs(dy)) { if (dx > 0) { napravlenieTekstom(2); } else { napravlenieTekstom(1); } } else { if (dy > 0) { napravlenieTekstom(4); } else { napravlenieTekstom(3); } } } delay(300);}Разберём арифметику. dx и dy — отклонения от откалиброванного центра, abs() берёт модуль, чтобы на этом шаге не думать о знаке. Пока оба отклонения внутри мёртвой зоны — ручка считается стоящей в центре. Как только хоть одно вылезло за порог — сравниваем модули: чья ось отклонилась сильнее, та и рулит. Диагоналей в этой схеме нет, и для манипулятора они не нужны: «влево и чуть вверх» всё равно раскладывается в два независимых действия по одной оси за раз. На плоттере мёртвая зона видна лучше всего: качните ползунок и посмотрите, как ряд x выходит из полосы шириной в восемь процентов диапазона — всё, что внутри полосы, скетч считает тишиной.
Знак определяет сторону. У моего модуля наклон вправо увеличивал x — вплоть до 1023, а наклон вверх уменьшал y — к нулю. Если у вашего модуля наоборот, просто поменяйте местами стороны сравнения: это не баг, а распайка конкретного экземпляра, я на двух своих так и сделал.
Заметьте: в этом скетче кнопка опрашивается в лоб, и пока держишь ручку — Klik печатается каждые 300 мс. Для учебного лога это даже наглядно, а для устройства — спам. Лечится фронтом: сравниваем текущее значение с предыдущим, печатаем только на переходе HIGH в LOW. На реальной плате этого мало: контакты дребезжат 5–20 мс после замыкания, и одно нажатие читается как несколько — антидребезг из урока про дребезг сюда ставится без переделок.
Номер направления 0–4 напрашивается на switch: четыре case и default — ровно та форма, которую разбирали в уроке про switch/case, и в финальной версии манипулятора я так и переписал: switch по направлению, в ветвях — вызовы функций поворота. А когда направлений станет шестнадцать, по четыре на сторону, вы придёте к матричной клавиатуре — она следующая по плану раздела, и там сканировать придётся уже строками и столбцами, без единого аналогового входа.
Грабли и живой прогон
- Найдите центр своего модуля: запустите скетч, не трогая ползунки, и посмотрите на строки centerX= и centerY= в начале лога. На реальной плате сделайте то же Serial-монитором — цифры будут вашими личными, а не бумажными 512.
- Сузьте мёртвую зону до 10 и поводите ползунок около центра: направления начнут мелькать от шума. Верните 40 и сравните, как успокаивается лог. Так порог подбирается под конкретный модуль, а не по статье.
- Поменяйте оси местами в скетче: читайте ось X с пина A1. Направления встанут крест-накрест — вы своими руками соберёте симптом перепутанного шлейфа из note и запомните его надолго.
- Прицепите к направлениям переменную угла: лево — минус пять градусов, право — плюс пять, центр — стоп, и ограничьте угол диапазоном 0–180. Это и есть готовый вход для серво-манипулятора из первого абзаца урока.
- KY-023 — два потенциометра 10 кОм и тактовая кнопка: VRx на A0, VRy на A1, SW на D2 с INPUT_PULLUP (нажата — LOW, в покое — HIGH).
- Ось — это АЦП 0–1023: в центре около 512, на краях 0 и 1023. Точный центр у каждого экземпляра свой — калибруйте средним из 20 замеров при старте, это 2 мс.
- Мёртвая зона глушит дрожь: отклонение меньше порога — ручка считается в центре. Мой порог — 40 для дешёвых модулей, у фирменных хватает 20, при вибрации моторов — 60.
- Направление выбирает ось-победитель: модуль |dx| больше — горизонталь, иначе вертикаль; знак говорит сторону. Диагоналей нет — раскладывайте движение на оси по очереди.
- Печатайте парами x=… y=… knopka=…: монитору это даёт читаемый лог, плоттеру — именованные ряды, а вам — живую картину дрожи и мёртвой зоны.
- 1. Ручка стоит ровно в центре. Что вернёт analogRead(A0) модуля KY-023?
- 2. Ручка стоит в центре, а курсор на экране дрожит. Какое решение — из этого урока?
- 3. Кнопка ручки подключена к D2, pinMode(2, INPUT_PULLUP) выполнен. Что вернёт digitalRead(2) при нажатии?
- 4. Наклоняете ручку влево-вправо, а в Serial меняется только ряд y. Где искать причину?
Частые вопросы по теме урока
- Ручка стоит ровно в центре. Что вернёт analogRead(A0) модуля KY-023?
- Ось — потенциометр между 5 В и землёй: в центре его движка analogRead даёт около половины диапазона. Точная цифра у каждого экземпляра своя — на это и нужна калибровка. Близкое к 512 — половину диапазона 0–1023
- Ручка стоит в центре, а курсор на экране дрожит. Какое решение — из этого урока?
- Дрожь в центре глушится мёртвой зоной: отклонения меньше порога игнорируются. Шум дешёвых потенциометров в 2–3 значения плюс физическая дрожь руки спокойно выбирают порог 40. Увеличить мёртвую зону, например с 20 до 40 значений
- Кнопка ручки подключена к D2, pinMode(2, INPUT_PULLUP) выполнен. Что вернёт digitalRead(2) при нажатии?
- Кнопка замыкает пин на GND, подтяжка держит его на HIGH в покое: нажата — LOW. Логика инвертированная, зато не нужны внешние резисторы. LOW (0)
- Наклоняете ручку влево-вправо, а в Serial меняется только ряд y. Где искать причину?
- Оси поменялись местами — фирменный симптом перепутанного шлейфа VRx и VRy. Проверяется за минуту: поменяйте A0 и A1 местами и посмотрите, переехал ли симптом. В проводах: на A0 на самом деле приходит потенциометр оси Y