Мини-манипулятор на моей макетке прожил неделю с тремя прошивками: первая вела серво от ручки, вторая гоняла его по дуге для обзора комнаты, третья стояла на месте и ждала команды зафиксировать угол. Заливать скетч под каждое настроение — путь в никуда, и я сел прикручивать кнопку режимов. Выбора не было: единственная кнопка на модуле — та, что под стержнем джойстика, и она обязана была стать селектором. Кнопка под стержнем — идеальный кандидат: она под большим пальцем ровно там, где рука лежит на ручке, и отдельный провод тянуть не нужно.
Первая версия посчитала одно нажатие четырьмя: тактовая кнопка дребезжит контактом 10-20 миллисекунд, и счётчик улетал вперёд, пока я отпускал палец. Вторая версией называлась с натяжкой: гася дребезг delay(100) прямо в loop, я замораживал серво на сто миллисекунд после каждого нажатия, и авто-обзор заикался на каждом щелчке. Рабочей стала третья: явный фронт-детектор и дебаунс по millis, без единого delay.
Разберём обе схемы: честную на прерывании для реальной платы, где SW сидит на D2, и опросную для симулятора, где автоподбор отдаёт SW на A2. Соберём три режима — ручное управление, авто-прогулка и фиксация позиции, — добавим пик зуммером на каждое нажатие и заведём переменные режима и сохранённого угла. Пик, светодиод и кадры Serial — три обратные связи, которые превращают слепое нажатие в ощутимое событие; без них режимы путаются уже на втором щелчке.
Счёт нажатий: фронт, дебаунс, прерывание
Счётчик строится на трёх деталях. Фронт: нажатие фиксируем один раз при переходе из HIGH в LOW — уровень LOW сам по себе ничего не значит, пока ручку держат, счётчик обязан молчать. Дебаунс: между фиксациями пауза 25-30 миллисекунд по millis — дребезг внутри паузы не виден. Память: предыдущее состояние кнопки хранится в переменной, без него фронт не отличить от уровня. Ту же конструкцию мы собирали в уроке про дебаунс кнопки. Окно 25-30 миллисекунд не случайно: дребезг тактовой кнопки укладывается в 10-20 миллисекунд, а людское «двойное нажатие» начинается после 80-100 — в коридоре между ними пауза глушит дребезг и пропускает честные щелчки.
Прерывание снимает с loop обязанность опрашивать кнопку: attachInterrupt(digitalPinToInterrupt(2), swIsr, FALLING) вызывает обработчик на каждый фронт, и главный цикл остаётся свободным для серво. Но у Uno внешние прерывания живут только на D2 и D3 — на аналоговых входах их нет вовсе, поэтому для версии на прерывании провод SW пересаживается на D2. Основы — режимы FALLING, RISING, CHANGE и volatile — разобраны в уроке про attachInterrupt.
Грабли прерываний, которые я собрал на этом меню, короткие, но болезненные. Первая: tone() внутри обработчика прерывания — tone сама работает на прерывании таймера, и вызов из ISR глушит звук или вешает плату; пик ставьте в loop, где смена режима ловится сравнением переменной. Вторая: забытый volatile — переменная, которую пишет обработчик, а читает loop, без volatile компилятор держит в регистре копию, и режим будто застревает. Третья: длинный обработчик — две-три строки сравнений в ISR ок, но Serial или тяжёлая арифметика там дают странно зависающий скетч. Все три ловятся за вечер, если знать, где искать.
// SW на D2: прерывание FALLING считает режимы, loop ведёт серво #include <Servo.h> Servo motor; volatile int rezhim = 1; // 1 ручное, 2 авто, 3 позицияvolatile unsigned long posledneeNazhatie = 0; void swIsr() { // FALLING: фронт из HIGH в LOW unsigned long sejchas = millis(); if (sejchas - posledneeNazhatie > 250) { // дебаунс прямо в обработчике rezhim = rezhim + 1; if (rezhim > 3) rezhim = 1; posledneeNazhatie = sejchas; }} void setup() { Serial.begin(9600); pinMode(2, INPUT_PULLUP); // SW замыкает на GND: в покое HIGH attachInterrupt(digitalPinToInterrupt(2), swIsr, FALLING); pinMode(12, OUTPUT); motor.attach(11); motor.write(90);} void loop() { static int poslednijRezhim = 0; static long poslednijShag = 0; static long avtoUgol = 90; static long napravl = 1; static long savedUgol = 90; if (rezhim != poslednijRezhim) { // режим сменился — пик и строка if (poslednijRezhim == 3) { // выход из режима 3: фиксируем угол savedUgol = motor.read(); Serial.print("saved="); Serial.println(savedUgol); } poslednijRezhim = rezhim; tone(8, 1200, 30); // пик подтверждения, 30 мс Serial.print("rezhim="); Serial.println(rezhim); } if (rezhim == 1) { // ручное: ось X в угол int x = analogRead(A0); if (x < 480 || x > 540) { motor.write(map(x, 0, 1023, 0, 180)); } } else if (rezhim == 2) { // авто-прогулка: шаг раз в 20 мс if (millis() - poslednijShag > 20) { poslednijShag = millis(); avtoUgol = avtoUgol + napravl; if (avtoUgol >= 180) napravl = -1; if (avtoUgol <= 0) napravl = 1; motor.write(avtoUgol); } } else { // позиция: ручка задаёт угол int x = analogRead(A0); motor.write(map(x, 0, 1023, 0, 180)); } if (rezhim == 3) { digitalWrite(12, HIGH); } else { digitalWrite(12, LOW); } delay(20);}Три режима и один счётчик
Режим — это просто число в переменной: 1 ручное управление, 2 авто-прогулка, 3 фиксация позиции. Вся развилка живёт в switch-case — конструкцию мы разбирали в уроке про switch и режимы. В ручном режиме ось X переписывает угол с зоной покоя 480-540; в авто-прогулке серво идёт по дуге 0-180 с шагом градус за цикл и разворачивается на краях — тот же приём, что в уроке про качание серво, только запускаемый кнопкой, а не прошивкой.
Третий режим — фирменная фишка: ручка задаёт угол свободно, без зоны, а нажатие кнопки фиксирует текущий угол в переменной savedUgol и возвращает плату в ручной режим. Получается запомнить кадр: наклонили камеру, нажали — угол записан, серво стоит на нём, пока ручка в покое. Светодиод на D12 горит только в режиме позиции — рука без экрана видит, что следующее нажатие запишет кадр, а не переключит режим. Родственная механика — шаг серво отдельными кнопками — разобрана в уроке про шаги кнопками.
Переключение режимов я поначалу делал бесшовным, а потом нарочно оставил ступеньку: выход из авто-прогулки на середине дуги — с avtoUgol, скажем, 137 — возвращает ручной режим, и зона покоя тут же тянет серво к savedUgol. Прыжок на десятки градусов за долю секунды я сначала считал дефектом, а теперь читаю его как подтверждение: платформа дёрнулась — значит, кнопка принята. Нужен мягкий переход — догоняйте целевой угол шагами по два градуса, как в прошлом уроке кластера: три строчки кода, дуга 137-90 займёт секунду с четвертью.
Отдельно про вход в режим 3: savedUgol при этом не трогается, серво слушается ручки без зоны, а прошлая фиксация ждёт следующего нажатия — повторный кадр перезапишется поверх старого числа. Мне так удобнее, чем очищать сохранение каждый раз: камера, однажды наведённая на окно, возвращается туда одним нажатием.
| Состояние | Что делает серво | Светодиод D12 | Кадр в Serial |
|---|---|---|---|
| Режим 1 — ручное | ось X в угол, зона покоя держит savedUgol | погашен | rezhim=1 ugol=… |
| Режим 2 — авто-прогулка | шаг 1 градус за цикл 20 мс, разворот на 0 и 180 | погашен | rezhim=2 ugol=… |
| Режим 3 — позиция | джойстик задаёт угол свободно, без зоны | горит | rezhim=3 ugol=… |
| Нажатие в режиме 3 | угол записан в savedUgol, возврат в режим 1 | гаснет | saved=… |
Пик подтверждения и кадры состояния
Пик подтверждения — одна строка: tone(8, 1200, 30). Тридцать миллисекунд на частоте 1200 герц палец слышит как сухой щелчок: короче — звук не успевает начаться, длиннее трёхсот — начинает раздражать при каждом переключении. Зуммер садится на любой свободный цифровой пин через резистор 100-220 Ом; вся арифметика частот и длительностей — в уроке про tone. В симуляторной макетке зуммера нет и пик не слышен, зато на живой плате рука без экрана понимает: нажатие принято, режим сменился.
Состояние печатаем в Serial кадрами rezhim= и ugol=, а фиксация добавляет строку saved= с числом. Пары имя=значение одинаково удобны монитору и плоттеру, а столбик кадров за пять секунд показывает характер поведения: ugol=, идущий лесенкой по единице, выдаёт авто-прогулку, скачки — ручку. Хранится режим и угол в обычных переменных — и умирают при перезагрузке; куда их положить надолго, разговор ниже.
От Serial-лога я ждал большего, чем получил: кадры rezhim= ugol= каждые 20 миллисекунд превращают монитор в простыню, глазами за щелчком не уследить. Спасли два приёма. Первый — печатать кадр только при смене режима: флаг разрешает вывод на один цикл после нажатия, и лог сжимается с пятидесяти строк в секунду до двух-трёх на щелчок. Второй — Plotter с каналом ugol=: авто-прогулка выглядит честной пилой от 0 до 180, а фиксация — ступенькой, застывшей на saved=.
Переменная режима доживает до перезагрузки питания ровно ноль секунд: после сброса всё встаёт в режим 1 с углом 90. Если выбранный режим и сохранённый угол должны переживать включение, их место в EEPROM — байт режима и байт угла пишутся за миллисекунды и держатся десять лет без питания. Готовый приём хранения настроек меню разобран в уроке про меню настроек в EEPROM — кнопка щёлкнула, режим записан, плата выключена, режим остался.
Меню шире трёх пунктов строится тем же счётчиком: каждое нажатие двигает указатель, ось Y листает список, длинное удержание выбирает. А когда кнопок станет несколько, прерывание с D2 расширится до второй линии D3 — и опрос в loop снова станет свободным. Правило одно на все варианты: состояние — в переменных, печать — кадрами парами, время — по millis, и ни одного delay на пути серво.
- 1. Счётчик насчитал четыре нажатия вместо одного. Самая вероятная причина?
- 2. На каких пинах Uno доступны внешние прерывания?
- 3. Что делает нажатие кнопки в режиме 3?
- 4. Какой пик подтверждения различим и не раздражает?
- 5. Где хранить выбранный режим, чтобы он пережил выключение питания?
- Счёт нажатий — это фронт HIGH-LOW плюс дебаунс 25-30 мс по millis: уровень и пауза, а не delay в loop.
- Внешние прерывания на Uno живут на D2 и D3: для версии на FALLING провод SW пересаживается туда, в симе SW на A2 опрашивается.
- Три режима — три ветки switch: ручное с зоной покоя, авто-прогулка градус за цикл, позиция со свободной ручкой.
- Нажатие в режиме 3 пишет текущий угол в savedUgol и возвращает ручной режим: кадр зафиксирован, светодиод показывал запись.
- tone(8, 1200, 30) — пик 30 мс на подтверждение; кадры rezhim= ugol= saved= идут в Serial, а EEPROM сохранит их между включениями.
Частые вопросы по теме урока
- Счётчик насчитал четыре нажатия вместо одного. Самая вероятная причина?
- Тактовая кнопка дребезжит 10-20 мс, и каждый фронт считается нажатием. Пауза 25-30 мс по millis между фиксациями гасит дребезг. дребезг контактов без дебаунса
- На каких пинах Uno доступны внешние прерывания?
- У Uno внешние прерывания — D2 и D3. attachInterrupt на остальных пинах не сработает: для версии на прерывании SW пересаживается на D2. только на D2 и D3
- Что делает нажатие кнопки в режиме 3?
- Режим 3 — режим записи: ручка свободно задаёт угол, нажатие сохраняет его в переменную и возвращает режим 1, где покой держит сохранённый угол. фиксирует текущий угол в savedUgol и возвращает ручной режим
- Какой пик подтверждения различим и не раздражает?
- Тридцать миллисекунд на 1200 герц слышатся как сухой щелчок: звук успевает начаться и не давит при частых переключениях. tone(8, 1200, 30) — 30 миллисекунд
- Где хранить выбранный режим, чтобы он пережил выключение питания?
- RAM очищается при сбросе. EEPROM держит байты без питания годами: байт режима и байт угла записываются за миллисекунды. в EEPROM