Манипулятор с ручным управлением я собирал по канону из урока про потенциометр: ручка на A0, map — на серво, всё по учебнику. Работало идеально ровно до первого дня эксплуатации: серво следила за ручкой с точностью до единицы АЦП, дёргаясь на каждый шорох пальца, редуктор щёлкал десятки раз в минуту, а к концу недели в приводе появился заметный люфт. Пластиковые шестерни SG90 не рассчитаны на ходы, которых никто не просил. База — тот самый map с ручки на серво — здесь не трогается: меняется только момент отправки команды.
Гистерезис — это мёртвая зона вокруг текущего положения: серво едет только тогда, когда ручка ушла на три и больше единицы АЦП. Всё, что мельче, — дрожь пальца, наводка, шорох — до серво не доходит. Следование остаётся, исчезает суета: число ходов редуктора падает в разы, а на глаз движение выглядит даже спокойнее, потому что рог перестаёт вторить каждому микродвижению руки. Звук работы меняется сразу: редуктор перестаёт щёлкать на каждом шорохе — это слышно без всяких приборов. В манипуляторе из вступления после установки зоны редуктор затихал вовсе: за неделю — только рабочие ходы.
Гистерезис пришёл в электронику из бытовой техники: термостат с зоной в полградуса не щёлкает реле на каждое колебание, а ждёт, пока нагрев или остывание перевалит за порог. Серво с ручкой живёт так же: текущее положение — центр зоны, чтение АЦП — температура, ход мотора — щелчок реле. Зона снимает суету, но не отменяет следование: большую разницу она пропускает мгновенно.
Арифметика мёртвой зоны
Зону удобнее мерить в единицах АЦП, а не в градусах. На Uno десять бит: 1023 единицы на полный ход 180 градусов, одна единица — примерно 0,18 градуса, три единицы — около полуградуса на валу. Целые числа без дроби, шум АЦП тоже целочисленный, сравнение однострочное: модуль разницы между свежим чтением и последним отправленным. Направление роли не играет — ушла ли ручка вверх или вниз на три деления, серво поедет. Один градус — это 5,7 единицы АЦП: держите это число в голове, когда переводите пороги в углы для отчёта. Порог хранится в одной константе GAP — правится в одном месте, без перелопачивания скетча.
Цена честная: серво отстаёт от ручки на величину зоны, до полуградуса при GAP=3. Для индикатора и манипулятора это незаметно; для прицела или фокуса объектива — уже нет. И не путайте с накоплением: если ручка ползёт по единице за раз, зона не съест ход — серво догонит на первом же шаге сверх порога. Мы отделяли полезное движение от дрожи и в уроке про интерполяцию без delay, только там резали шаги во времени, а здесь — по величине. Термостат из вступления ведёт себя так же: зона не мешает ему догнать комнату, остывшую на пять градусов, — порог проходится с запасом.
Откуда берётся мелочь, которую мы отсекаем? Из трёх источников: шум самого АЦП в единицу-две, наводки на провод ручки и дрожь руки оператора. Каждый сам по себе мал, но сырой скетч превращает сумму в микродвижения рога — контур внутри серво получает новую цель каждые двадцать миллисекунд и честно отрабатывает её. Гистерезис стоит на границе между ручкой и серво и не пропускает суету дальше.
#include <Servo.h> // Ручка A0 ведёт серво, но ход только при уходе на 3+ единицы АЦП.// Кнопка D2 захватывает угол немедленно, минуя мёртвую зону. Servo horn; const int BTN = 2;const int GAP = 3; // мёртвая зона, единиц АЦП int lastVal = -1000; // значение, при котором серво двигаласьint lastBtn = HIGH;unsigned long t = 0; void setup() { horn.attach(11); pinMode(BTN, INPUT_PULLUP); Serial.begin(9600); Serial.println("A0: ruchka, D2: zahvat seichas");} void loop() { int nowBtn = digitalRead(BTN); if (lastBtn == HIGH && nowBtn == LOW) { // форс-синхронизация lastVal = analogRead(A0); horn.write(map(lastVal, 0, 1023, 0, 180)); Serial.print("zahvat: val="); Serial.println(lastVal); } lastBtn = nowBtn; if (millis() - t >= 30) { // опрос тридцать раз в секунду t = millis(); int val = analogRead(A0); int diff = val - lastVal; if (diff < 0) { diff = -diff; } if (diff >= GAP) { // порог пройден - едем lastVal = val; horn.write(map(val, 0, 1023, 0, 180)); Serial.print("hod: val="); Serial.print(val); Serial.print(" deg="); Serial.println(map(val, 0, 1023, 0, 180)); } }}Порог держите в переменной GAP — его подбирают под задачу, а не выводят из теории. Три единицы — для ручки: дрожь пальца гасится, следование живое. Пять — для джойстика, который сам подшумливает на центре. Десять и больше — для датчиков: уличный фоторезистор или аналоговый микрометр шевелятся сильнее, чем палец, и без широкой зоны такой источник разорит редуктор.
Заметьте: write() вызывается только внутри условия. Серво не получает команды в каждом проходе, и контур не гоняется за микропоправками — в этом половина эффекта. Опрос раз в тридцать миллисекунд ручку не замедляет, зато АЦП успевает осесть после смены положения. Печать hod= показывает каждое срабатывание порога: на живой ручке таких ходов за минуту в разы меньше, чем сырых изменений чтения. Печать deg= рядом с val= напоминает: в серво уходит округлённый угол, а порог сравнивается в сырых единицах — держите масштаб единым. Если строк hod= слишком много — порог мал; если ручка тяжелеет — велик: это и есть настройка по логу.
Связь с прошлым уроком прямая: усреднение чистит само чтение, гистерезис решает, стоит ли ехать. Вместе они дают индикатор, который стоит на месте часами и не изнашивает редуктор. А если дрожь у серво осталась и с неподвижной ручкой, перечитайте урок про помехи и питание — кодом такое не лечится.
Эффект удобно мерить счётчиком. Я вешал на hod= счётчик срабатываний за минуту: сырой скетч на спокойной руке давал сорок-шестьдесят ходов, скетч с зоной — три-пять. Разница на порядок — это и есть срок жизни пластиковых шестерён, растянутый в те же десятки раз, плюс тишина вместо щёлканья. Счётчик пишется одной переменной и удаляется после замера.
| GAP, единиц АЦП | Зона на валу | Поведение | Куда годится |
|---|---|---|---|
| 1 | около 0,18° | Следование почти непрерывное, дрожь проходит лишь частично: контур ещё суетится | Точные приводы с чистым питанием |
| 3 | около 0,53° | Ручка ведёт, дрожь и шорохи отсечены порогом: ход только за границей зоны | Индикаторы, манипуляторы, ручное управление |
| 5 | около 0,9° | Заметно спокойнее, отставание от ручки ощутимо | Джойстики и датчики с шумом |
| 10 | около 1,8° | Грубое ступенчатое следование: команды мельче двух градусов теряются | Уличные приборы с перепадами освещения |
Гистерезис в симуляторе
Модель повторяет скетч: ручка, порог три единицы, кнопка захвата. Попробуйте главное — шевелите ручку в пределах трёх делений: в Serial тишина, рог стоит. Уведите ручку на четверть шкалы — один ход с печатью hod= и замирание на новом месте. Затем уводите ручку мелкими шажками по две единицы: серво пропустит шаг и поедет только тогда, когда накопленная разница с последним отправленным значением перевалит за порог. Если Serial молчит, а рог дёргается сам — зона тут не при чём: вернитесь к питанию из прошлого урока. На гладкой ручке без шума зона почти незаметна — на живой руке она слышна по тишине редуктора.
Кнопка D2 — про случай, когда синхронизироваться нужно немедленно: смена оператора, новая задача, калибровка. Нажатие записывает текущее чтение и угол без всякого порога. Захват пригодится и после смены GAP: порог поправили — синхронизировались вручную. Для сравнения откройте базовый скетч ручного управления: там серво следует непрерывно, и ход приходится на каждое деление — разница в числе срабатываний за минуту видна в Serial невооружённым глазом. А таймер опроса на millis() оставляет loop() свободным — можно смело наращивать скетч.
Поиграйте порогом: смените GAP с трёх на десять — зона вырастет почти до двух градусов, и ручка станет ощутимо ступенчатой. Верните тройку и попробуйте вести ручку очень медленно: серво доедет до конца шкалы, пусть и с отставанием в полделения. Мёртвая зона никогда не стопорит следование насовсем — она лишь требует уверенного шага.
- Гистерезис — мёртвая зона вокруг последнего отправленного значения: серво едет только при уходе ручки за порог
- 3 единицы АЦП — около полуградуса на валу: следование живое, дрожь пальца отсечена
- write() только внутри условия: контур серво перестаёт гоняться за микропоправками
- Порог подбирают под источник: 3 — ручка, 5 — джойстик, 10+ — шумный датчик
- Меньше ходов — дольше живёт пластиковый редуктор SG90: гистерезис экономит и шестерни, и ток
- 1. Мёртвая зона 3 единицы АЦП — сколько это на валу?
- 2. Ручку шевельнули на две единицы от последнего отправленного значения. Что делает скетч?
- 3. Зачем в скетче кнопка форс-захвата?
- 4. Что случится при GAP=20?
- 5. Почему гистерезис продлевает жизнь редуктора?
Частые вопросы по теме урока
- Мёртвая зона 3 единицы АЦП — сколько это на валу?
- 1023 единицы на 180 градусов: одна единица — примерно 0,18°, три — около полуградуса. Полтора градуса — это зона в восемь-девять единиц. Пять единиц — уже около 0,9°. Около 0,53°
- Ручку шевельнули на две единицы от последнего отправленного значения. Что делает скетч?
- Две единицы меньше GAP=3: чтение уходит как фон, серво не получает ни команды, ни движения. Порог сработает, когда разница с последним отправленным дойдёт до трёх. Ничего: порог не пройден
- Зачем в скетче кнопка форс-захвата?
- Кнопка синхронизирует lastVal и рог с текущим чтением напрямую: полезно, когда нужно стартовое совпадение ручки и серво, а ждать накопления порога некогда. Записать угол немедленно, минуя мёртвую зону
- Что случится при GAP=20?
- Двадцать единиц — почти 3,6 градуса мёртвой зоны: мелкие команды теряются, следование становится ступенчатым. Для ручки комфортный потолок — пять-семь единиц. Серво отстанет от ручки на 3,6°: управление станет грубым
- Почему гистерезис продлевает жизнь редуктора?
- Каждое лишнее микродвижение — щелчок пары пластиковых шестерён, а люфт у SG90 необратим. Отсекая дрожь, зона сокращает число ходов в разы — и ток заодно: каждый ход это бросок до сотен миллиампер. Ходов меньше: пластиковые шестерни изнашиваются медленнее