Я подбирал качалку под самодельный манипулятор и двадцать минут жил в цикле: правю число в скетче, жму загрузку, жду компиляцию, смотрю, не то — снова. Угол 45? Нет, 55. Пять перепрошивок ради одного сустава, и на пятой я поймал себя на мысли, что делаю работу калькулятора вручную. А выход был в меню Tools всё это время: монитор порта — самый дешёвый пульт в вашей практике, он уже встроен в IDE и умеет отправлять числа прямо в бегущий скетч. Вводите угол — серво едет; не подошло — вводите другой, без единой перепрошивки. Монитору порта не нужны ни кнопка, ни пульт, ни потенциометр — только строка в окне IDE, которое и так открыто: для калибровки механики идеального инструмента не придумать.
В этом уроке научим серво слушать порт. Разберём, как Serial.parseInt() превращает буквы в число, почему без constrain() в серво уедут и −45, и 999, и разберу вслух свою любимую ловушку: серво, которая сама уезжала в ноль после каждой команды, — на разборе этой ловушки вы поймёте про буфер порта больше, чем из любого справочника. Понадобится только уметь открывать монитор порта из урока про первый запуск Serial и помнить, что делают attach() и write(), из урока про библиотеку Servo.
parseInt: из букв в число
В порт приходит текст, а серво нужен целый угол, и мостик между ними — Serial.parseInt(). Работает он по простому рецепту: пропускает всё, что не похоже на начало числа, потом собирает цифры, пока не встретит посторонний символ. Строка "105" превращается в 105, строка "abc90" — тоже в 90: ведущие буквы он молча пропустил. Если будете посылать несколько чисел в одной строке — "90 120" — parseInt прочтёт их по очереди при последовательных вызовах: два серво — два вызова, дорога к роборуке начинается ровно здесь. Ведущий минус читается как знак числа, поэтому "-45" вернёт −45 — запомните эту деталь, через минуту она выстрелит.
Вторая особенность важнее первой: если данных в порту нет, parseInt не возвращает управление, а ждёт — по умолчанию до 1000 мс — и лишь потом отдаёт 0. На реальной плате это значит, что один необдуманный вызов съедает секунду из каждого прохода loop, а мигающий светодиод начнёт мигать с запинкой. Правило, которое я выбил себе в привычку: дёргать parseInt только после проверки if (Serial.available() > 0). Скорость тут не мешает: при 9600 бод символ едет около миллисекунды, даже строка "105\n" — это 4 мс. Буфер у Uno, кстати, всего 64 байта: если отправить длинную простыню, хвост будет теряться — но нам про неё и не надо, один угол укладывается в четыре символа с запасом. И ещё раз про available(): он отвечает «в буфере что-то есть», а не «в буфере лежит целое число» — байт и число не одно и то же, и вся ловушка с HC-05 выросла именно отсюда.
| Вы вводите | parseInt получит | Комментарий |
|---|---|---|
| 90 | 90 | чистое число, скучный случай |
| abc90 | 90 | ведущие буквы пропускаются |
| 90abc | 90 | разбор обрывается на первой букве |
| -45 | -45 | минус читается как знак — дальше спасает constrain |
| 250 | 250 | parseInt не знает про серво: режет его constrain |
| (ничего) | 0 после 1000 мс | таймаут parseInt — вот почему нужен available() |
Тупик, который я помню телом. Подключил серво к Uno через Bluetooth-модуль HC-05, отправил "105" — серво приехала на 105 и тут же уехала в 0. Я трижды перепрошил скетч, заподозрил провод, потом батарейку приёмника. А виноват был parseInt: у HC-05 байты приезжают по одному, available() сработал на первом байте, число ещё не собралось — parseInt дождался таймаута и вернул 0, который честно уехал в серво. С тех пор в моих скетчах стоят обе защиты сразу: available() перед чтением и constrain(ugol, 0, 180) после. Вторая — не бюрократия: серво, подпёртая рычагом, на рывке к упору тянет 650–700 мА, и каждый такой рывок бьёт по питанию и шестерням.
И третья защита — дочистка буфера. Монитор порта вместе с числом шлёт перевод строки, parseInt читает цифры и оставляет хвост в буфере. Следующий проход loop видит available() больше нуля, parseInt не находит цифр, ждёт секунду и возвращает 0 — уехать в ноль может и без всякого Bluetooth. Одна строка while (Serial.available() > 0) Serial.read(); после разбора сгорает хвост вместе с проблемой. Я считаю, что эта строка должна быть в каждом скетче с parseInt — экономии в три токена против вечера дебага она окупается уже на второй команде.
Скетч и симулятор
Скелет скетча — три строки внутри loop: прочитали число, зажали его в допустимую вилку, отдали серво. Порядок в setup() свободный, но я всегда ставлю Serial.begin(9600) первым: приглашение "Vvedite ugol" обязано напечататься после открытия порта — при обратном порядке первые строки теряются в момент поднятия линии, и вы смотрите в пустой монитор, хотя скетч уже работает. Ноль при этом остаётся легальным углом: 0° — честная позиция качалки, запрещать её не нужно, но читать её скетч будет только тогда, когда вы действительно отправили цифру — дочистка хвоста убирает самозваные нули таймаута. Ответ печатаем парами вида ugol=105: по такой строке значение сразу видно и в мониторе, и в плоттере, где на нём удобно ловить скачки — например, те самые самозваные нули, если вы их ещё не дочистили. И держите скорость 9600 одинаковой в begin и в селекторе монитора: рассинхрон покажет мусор вместо чисел.
Кстати, родственник этого скетча уже жил у нас в разделе про Serial: светодиод, яркость которого задают числом из порта, — та же схема на один пин проще, и она разобрана в уроке про светодиод из Serial. Серво добавляет к ней физику: число превращается в движение, движение — в механику с упорами, поэтому вилка constrain здесь не перестраховка, а охрана шестерён. Если серво нужна мягкая, а не рывковая езда — возьмите плавный ход из урока про плавное движение серво и интерполируйте угол между старым и новым значением: примите из порта цель, а едете к ней шагами.
Отдельно про тип переменной: parseInt возвращает long, но углы помещаются в int с запасом — байта бы даже хватило, однако минус 45 в byte уже не влезет, и я видел скетчи, где −45 после byte превращалась в 211 и уводила серво в самый неожиданный конец хода. Обе защиты — available() и constrain — в финальном скетче уже стоят, и в симуляторе вы сможете разбирать их по одной, чтобы увидеть, что именно ломается без каждой.
#include <Servo.h> Servo srv; void setup() { Serial.begin(9600); srv.attach(11); Serial.println("Vvedite ugol 0-180 i Enter");} void loop() { if (Serial.available() > 0) { // число уже едет — можно читать int ugol = Serial.parseInt(); ugol = constrain(ugol, 0, 180); // -45 и 999 до серво не доедут while (Serial.available() > 0) Serial.read(); // сгорел хвост строки srv.write(ugol); Serial.print("ugol="); Serial.println(ugol); }}Три упражнения на закрепление. Первое: отправьте −45 и посмотрите в мониторе, что ответила серво, — constrain съел минус, и это видно по строке ugol=0. Второе: уберите строку с дочисткой буфера и отправьте 105 с переводом строки — поймаете вживую самозваный ноль, теперь вы его узнаёте в лицо и знаете, где у него выключатель. Третье: повесьте на ввод угла плавное движение — серво должна не прыгать, а ехать к цели; ингредиенты лежат в уроке про плавное движение. Четвёртое, для тех, у кого две серво: принимайте два числа в строке и ведите оба сустава — два вызова parseInt подряд, дорога к роборуке. Когда рука набьётся на вводе числом, следующий урок этого раздела перенесёт пульт из окна IDE в ладонь — на инфракрасный пульт от машинки, где каждая кнопка станет шагом угла.
- 1. В скетче Serial.parseInt() вызван без проверки available(). Что будет, если в порт ничего не пришло?
- 2. Вы отправили 250. Что запишет srv.write() после constrain(ugol, 0, 180)?
- 3. Серво приехала на 105 и тут же уехала в 0. Самая вероятная причина?
- 4. Вы отправили abc90. Сколько вернёт parseInt?
- 5. Зачем после parseInt дочищают буфер?
- parseInt читает только после if (Serial.available() > 0): без проверки — секунда тишины и самозваный ноль.
- constrain(ugol, 0, 180) сразу после чтения: −45 и 999 не должны доехать до упоров.
- Дочистка буфера после разбора — страховка от второго прохода с нулём.
- abc90 — это 90: parseInt пропускает всё до первой цифры, мусор в порту становится числом.
- Печатайте ответ парами ugol=105 — сразу видно и в мониторе, и в плоттере.
Частые вопросы по теме урока
- В скетче Serial.parseInt() вызван без проверки available(). Что будет, если в порт ничего не пришло?
- Таймаут parseInt по умолчанию 1000 мс — секунда тишины в каждом проходе loop. Без available() каждый цикл теряет секунду — и это ещё и ноль на выходе. loop подвиснет: parseInt ждёт до 1000 мс и потом вернёт 0
- Вы отправили 250. Что запишет srv.write() после constrain(ugol, 0, 180)?
- constrain зажимает значение в вилку с обеих сторон: всё, что больше 180, становится 180. Без него write() приказал бы серво в упор, а при подпёртом рычаге ток скакнул бы к 650–700 мА. 180
- Серво приехала на 105 и тут же уехала в 0. Самая вероятная причина?
- Хвост посылки запускает второй разбор, который не находит цифр и возвращает 0 по таймауту. Дочистка буфера убирает эффект полностью, а available() перед чтением отсекает половину таких случаев. В буфере остался перевод строки, parseInt по таймауту вернул 0
- Вы отправили abc90. Сколько вернёт parseInt?
- parseInt молча пропускает всё, что не цифра, и собирает число с первой цифры. Это удобно и опасно: мусор в порту становится числом. 90 — ведущие буквы пропускаются
- Зачем после parseInt дочищают буфер?
- Оставшийся перевод строки даёт available() больше нуля и второй проход parseInt с таймаутом и нулём. Одна строка read() сжигает проблему. Чтобы перевод строки из хвоста не запустил второй разбор с нулём