Выдвижную полку стола с паяльным хозяйством я перевёл на серво осенью: SG90 на кронштейне, зубчатая тяга, скетч на десять строк — write(0), полка закрыта, write(170), открыта. Месяц всё работало идеально, а потом полка начала доезжать с металлическим щелчком, который я поначалу записал в фичи. Щелчком был рог, бьющийся о механический упор на полной скорости — по три-четыре раза в день, пятьдесят градусов хода с разгоном. Через две недели люфт качалки стал заметен пальцем: нейлоновые шестерни SG90 не прощают таких приездов, и это была самая дорогая экономия десяти строк кода в моей практике.
Масштаб разрушения тут такой: удар о стопор — это не трение, а инерция рычага, которая бьёт по зубьям в момент, когда двигатель уже остановлен границей. MG996R с металлическим редуктором такие встречи переживёт, но ему жалко уже не шестерни, а качалку и винт крепления. Дешевле всего проблема закрывается не железом, а двумя числами в скетче: не ездить до механического края вовсе, а останавливаться в паре градусов до него и подъезжать к этой границе замедляясь. Так у меня появились мягкие края — программные ограничители, о которых дальше вся речь.
Базу для этого урока я закладывал в двух местах: что делает команда за границу и почему constrain режет её молча — в уроке про упоры и constrain, а пошаговый ход по миллис, на котором тут всё ездит, — в уроке про шаг по градусу. Здесь надстраиваю следующий этаж: границы живут не в write(), а в отдельной паре констант, край тормозит, а концевик служит судьёй, если механика скажет «дальше нельзя».
Мягкий край: тормозим до границы, а не об неё
Механика мягкого края проще, чем кажется. Держу два числа: SOFT_MIN и SOFT_MAX — рабочий коридор внутри честного хода. Пока серво едет в середине коридора, шаг обычный — один градус каждые двенадцать миллисекунд. Как только до ближайшей границы остаётся меньше двадцати градусов, шаг растягивается: один градус за сорок миллисекунд. На языке скоростей это 83 градуса в секунду против 25 — последние сантиметры хода полка проползает, и даже если что-то пойдёт не так, удар получится втрое слабее.
Второй слой — сама цель. Ручка или команда могут просить угол за коридором: map от потенциометра честно выдаст и ноль, и сто восемьдесят. Мягкий край означает, что цель пережимается в коридор ещё до движения: попросили десять — едем до пятнадцати, попросили сто восемьдесят — до ста шестидесяти пяти. Все команды идут через одну функцию с constrain внутри; никаких прямых write(tsel) в скетче, иначе через неделю кто-то добавит ветку, которая пишет мимо ограничителя, — у меня так один раз и случилось.
Пятая графа расходов — запас. Мои границы стоят в пяти градусах от физических упоров не для красоты: механика живёт, усыхает, гуляет от сезона, и качалка, которой сегодня хватает запаса, зимой доедет на градус дальше. Если поставить SOFT_MAX вплотную к упору, один холодный месяц — и вы снова слушаете щелчок, только теперь ещё и с недоумением, куда делся ограничитель.
#include <Servo.h>#include <EEPROM.h> Servo polka; // D11; концевики: нижний D2, верхний D3 const int PIN_NIZ = 2;const int PIN_VERH = 3;const int ZAPAS = 5; // откат от щелчка — и есть мягкая границаconst unsigned long T_KALIBR = 40; // ползучий шаг калибровки, мс на градусconst byte MAGIK = 42; // признак «в EEPROM лежат настоящие границы» int granNiz = 15; // заводские оценки до первой калибровкиint granVerh = 165;int tek = 90;unsigned long tHod = 0; void zapisatGranicy() { EEPROM.update(0, MAGIK); EEPROM.update(1, granNiz); EEPROM.update(2, granVerh);} void chitatGranicy() { if (EEPROM.read(0) != MAGIK) return; // ещё не калибровали int n = EEPROM.read(1), v = EEPROM.read(2); if (n >= 5 && v <= 175 && n < v) { granNiz = n; granVerh = v; }} // ползём по градусу, пока концевик не щёлкнет; возвращаем угол щелчкаint poiskGranic(int napravlenie) { while (digitalRead(napravlenie < 0 ? PIN_NIZ : PIN_VERH) == HIGH) { if (millis() - tHod < T_KALIBR) continue; tHod = millis(); tek += napravlenie; polka.write(tek); if (tek <= 0 || tek >= 180) break; // страховка от сорванного концевика } return tek;} void kalibrirovka() { Serial.println("kalibr: poisk niza"); granNiz = constrain(poiskGranic(-1) + ZAPAS, 0, 180); delay(300); Serial.println("kalibr: poisk verha"); granVerh = constrain(poiskGranic(1) - ZAPAS, 0, 180); zapisatGranicy(); Serial.print("granicy: "); Serial.print(granNiz); Serial.print(".."); Serial.println(granVerh);} void setup() { Serial.begin(9600); pinMode(PIN_NIZ, INPUT_PULLUP); pinMode(PIN_VERH, INPUT_PULLUP); polka.attach(11); polka.write(tek); chitatGranicy(); Serial.print("granicy: "); Serial.print(granNiz); Serial.print(".."); Serial.println(granVerh); Serial.println("k - perekalibrovat");} void loop() { if (Serial.available() && Serial.read() == 'k') kalibrirovka(); int tsel = map(analogRead(A0), 0, 1023, granNiz, granVerh); // map сам держит коридор if (tek == tsel) return; int doKraya = (tsel < tek) ? tek - granNiz : granVerh - tek; unsigned long interval = (doKraya < 20) ? 40 : 12; // тормозим у края if (millis() - tHod < interval) return; tHod = millis(); tek += (tsel > tek) ? 1 : -1; polka.write(tek);}Автокалибровка делается ползучим ходом: серво идёт к упору по градусу каждые сорок миллисекунд, пока контакт не щёлкнет, потом откатывает пять градусов и записывает это число в память. Вся процедура занимает три-четыре секунды и блокирует loop — при старте платы я это прощаю, в рабочем цикле нет. Перекалибровка при каждом включении означала бы, что полка бьётся об упор на старте — ровно то, от чего мы уходим, поэтому границы живут в EEPROM: если magic-байт не наш, берутся заводские оценки, а не мусор из свежих ячеек. Саму схему magic-байта я разбирал в уроке про серво, которая помнит угол, а записи через update() не дёргают ячейку зря — про износ подробнее в уроке про update и износ ячеек.
Пара слов о выборе концевика. Для полки в метре от платы хватает любого тактильного с двумя контактами; если линия длинная или на улице, берите нормально закрытый — обрыв провода превратится в постоянное «граница найдена», а не в тихий сбой, полный разбор НО и НЗ я делал в уроке про ворота с концевиком. Дребезг контакта калибровке не страшен: щелчок ищется уровнем, а не фронтом, и откат на пять градусов выводит механизм из зоны болтанки — отдельные анти-дребезговые приёмы пригодятся, только если концевик работает как кнопка события в основном цикле.
| Стратегия | Что с шестернями | Ток при приезде | Когда выбираю |
|---|---|---|---|
| write(0) и write(180) в упор | удар каждый цикл, люфт растёт неделями | стопорный, у SG90 до 650 мА | никогда: только как приёмочный тест новой серво |
| Мягкий край на константах | упора не касается, люфт стоит на месте | обычный ход, 100-250 мА | всегда, если границы известны и стабильны |
| Концевик + откат 5° | самопроверка при каждом старте | короткий ход, 100-250 мА | границы плавают: усыхает дерево, отворачиваются винты |
| Ограничение по току двигателя | останавливает по пику тока | пик ловится, но серво уже ударилась | когда концевик поставить некуда |
В симуляторе логика живёт без железа: ручка задаёт сырую цель, диод показывает, что вас просят за коридор, а серво всё равно останавливается на мягкой границе и тормозит последние двадцать градусов. Кнопка играет концевик: щёлкните у края — механика скажет «дальше нельзя», серво откатит три градуса и напечатает это в монитор. На настоящей полке между кнопкой и концевиком разницы никакой: тот же INPUT_PULLUP, тот же LOW при нажатии, чтение пина переносится один в один из урока про кнопку.
Числа торможения подобраны под SG90 и полку весом в триста граммов: интервал сорок миллисекунд на градус даёт 25 градусов в секунду — глазами медленно, по инерции мягко. Для тяжёлой створки с MG996R я растягиваю зону до тридцати градусов и интервал до пятидесяти: чем больше масса на плече, тем раньше надо начинать тормозить. Универсальных цифр нет, есть один честный тест: поставьте стакан воды рядом с механизмом и прогоните десять циклов — если вода дрожит, край недостаточно мягкий.
Мягкий край лечит удар о границу, но не затор посреди хода: если в направляющую попала стружка, серво будет честно ползти к цели и упрётся в препятствие на полной скорости середины диапазона. От этого слоя защиты один шаг — тайм-аут: движение к цели, которое не закончилось за отведённое время, снимается с задачи, а автомат поднимает тревогу. Стопорный ток при заторе тот же, что при ударе: SG90 ест до 650 мА и греется уже через десятки секунд, бюджет питания под такой случай и поведение платы при провале я считал в уроке про пиковый ток и сброс.
Второй случай, где мягкий край бессилен, — просьба за границу из внешнего источника: расписания, чужого кода, монитора порта. Я ловил это дважды: скетч из туториала писал write(180) напрямую, и все мои константы наблюдали за этим со стороны. Дисциплина тут скучная, но железная: единственная функция в скетче, имеющая право вызвать write, — ограничитель, всё остальное передаёт ему сырую цель. Именно она держит механику целой, когда проект разрастается ветками и авторами.
Люфт, отладка и одна точка для угла
Люфт после ударов слышен раньше, чем виден: серво начинает гудеть на месте приезда, потому что побитые зубья дают рогу свободу в пару градусов, и контур постоянно их выбирает. Мой тест на износ простой: двигаю рог пальцем при снятом питании — свободный ход до щелчка зубьев и есть глубина повреждения. Полка после двух недель ударов имела три градуса люфта; после перехода на мягкие края прогресс остановился, дальше люфт не рос.
Границы я прогоняю на столе до установки: двадцать циклов от края до края, и смотрю два момента — доезжает ли серво до мягкой границы ровно, без удара, и что происходит при просьбе за коридор. Если при цели за границей слышен щелчок об упор, constrain стоит не на всех путях: чаще всего забывают ветку, которая пишет угол напрямую мимо функции-ограничителя. Одна точка прохода для угла — правило, которое экономит шестерни: заведите отдельную функцию с constrain внутри и не пускайте ни один write мимо неё, что бы ни добавляли в скетч дальше.
Мой чек-лист при установке на место короткий, но я его держу. Первое: после монтажа прокалибровать заново — геометрия кронштейна съедает пару градусов по сравнению со столом. Второе: прогнать десять циклов и потрогать серво — нагрев к третьему циклу говорит о механическом клине, который на столе не проявлялся. Третье: выключить и включить плату — границы должны подтянуться из EEPROM, а не искать щелчок заново; если при каждом старте слышна поездка до упора, что-то в схеме magic-байта я сломал.
- 1. Что именно ломает серво при работе «в упор»?
- 2. Как скетч тормозит у мягкой границы?
- 3. Ручка просит угол 5°, мягкая граница 15°. Куда поедет серво?
- 4. Что делает автокалибровка, найдя щелчок концевика?
- 5. Почему границы хранятся в EEPROM, а не пересчитываются при каждом старте?
- Мягкий край — пара констант и замедление в зоне 20°: удар о стопор исчезает целиком
- Все углы проходят через одну функцию с constrain: прямой write мимо ограничителя возвращает удары
- Концевик находит границу ползучим ходом: откат 5° и запись в EEPROM с magic-байтом
- Тормозная зона растёт с массой: полке 20° и 40 мс/°, тяжёлой створке 30° и 50 мс/°
- Люфт проверяется пальцем при снятом питании: свободный ход до щелчка зубьев
Частые вопросы по теме урока
- Что именно ломает серво при работе «в упор»?
- Двигатель остановлен границей, а рычаг по инерции продолжает давить: зубья принимают рывок. Мягкий край убирает сам удар, а не его последствия. Инерция рычага бьёт по зубьям в момент останова о стопор
- Как скетч тормозит у мягкой границы?
- Скорость задаётся интервалом между градусами: у края шаг растягивается, серво подъезжает медленно. detach тут ни при чём — удержание сохраняется. Растягивает шаг с 12 до 40 мс на градус в зоне у края
- Ручка просит угол 5°, мягкая граница 15°. Куда поедет серво?
- Мягкий край — это пережатие цели до коридора до всякого движения. Серво не знает, что её просили за границу, и потому не бьётся об упор. К 15°: цель пережимается constrain'ом до коридора
- Что делает автокалибровка, найдя щелчок концевика?
- Граница = щелчок минус пять градусов запаса. Именно запас делает мягкую границу живучей к сезонному дрейфу механики. Откатывает на запас и запоминает границу вместе с ним
- Почему границы хранятся в EEPROM, а не пересчитываются при каждом старте?
- Каждая калибровка — поездка до щелчка, то есть касание упора. Раз в месяц при переносе механики — да; на каждом включении — нет. Стартовая перекалибровка бьёт механизмом об упор — то, от чего уходим