Табло над дверью гаражного кооператива я собрал за вечер: LCD 16x2, Uno, занавес от солнца. А наутро заказчик — сосед по гаражу — спросил, как менять надпись, и я осёкся: перепрошивать плату ради смены «Открыто» на «Закрыто» — это же вынимать табло, тащить ноутбук и держать паузу в обслуживании. Выход был не в новом железе, а в канале, который уже есть на каждой плате: Serial. Монитор порта превращается в пульт — строка ушла с компьютера, экран перерисовался, никакого доступа к скетчу. При скорости 9600 бод один символ идёт около миллисекунды, так что команда «0:Privet mir» из двенадцати знаков доезжает за двенадцать миллисекунд — быстрее, чем глаз моргнёт.
Здесь мы построим этот пульт целиком: придумаем крошечный протокол из четырёх команд, научимся собирать строку из Serial посимвольно, не блокируя loop(), разберём эхо-ответы, по которым видно, что плата поняла, и сделаем в симуляторе живой стенд, где команды печатаются в монитор порта и появляются на экране. Если вы ещё не открывали монитор порта вообще, начните с урока про первый запуск Serial — там скорость, перевод строки и первые print().
Протокол: четыре команды
Экран не умеет читать мысли: без договора о формате он получит поток байтов и не поймёт, где тут адресат — верхняя строка, нижняя или вообще команда очистки. Договор я выбрал предельно тупой, и это осознанное мнение: парсить придётся не вам, а плате с её ATmega328, поэтому каждая хитрость протокола превращается в десяток строк кода и место для глюков. Четыре команды: «0:текст» — напечатать текст в верхнюю строку (нумерация строк с нуля, как в setCursor), «1:текст» — в нижнюю, «cls» — очистить экран, «bl1» и «bl0» — включить и выключить подсветку. Конец команды — перевод строки, та самая галочка Newline в мониторе порта, без которой пульт молчит.
Граница применимости у простоты тоже есть: если команд станет больше восьми-десяти или появятся параметры с числами, я перехожу от сравнения символов к разбору по шаблону с номером команды — это разобрано отдельно в уроке про командный парсер Serial. Для табло из четырёх команд мой вариант честнее: любое расширение потом, когда появится вторая команда, а не впрок.
| Команда в мониторе | Что делает плата | Ответ в Serial |
|---|---|---|
| 0:Privet mir | Печатает Privet mir с начала верхней строки | ok=row n=1 |
| 1:Temp 25 C | Печатает Temp 25 C в нижнюю строку | ok=row n=2 |
| cls | Очищает экран целиком | ok=cls n=3 |
| bl1 / bl0 | Включает / выключает подсветку | ok=bl n=4 |
| что угодно другое | Ничего не меняет на экране | ok=unknown n=5 |
Теперь чтение, и здесь главная развилка урока. Соблазнительный путь — Serial.readString(): одна строка кода, но у неё скверный характер: если данных нет, она ждёт таймаут — по умолчанию целую секунду замирает весь loop, а недошедшая строка склеивается со следующей. Я после пары вечеров с зависавшими кнопками ушёл на посимвольное чтение, как в уроке про Serial.available() и read(): пока available() больше нуля, вынимаем по символу и складываем в буфер line на 24 ячейки — по длине строки экрана с запасом. Увидели перевод строки — команда собрана, обрабатываем и обнуляем счётчик. Цикл остаётся живым между любыми командами, кнопки опрашиваются хоть каждый миллисекунду. Для сравнения, readStringUntil() с терминатором — компромиссный вариант, который мы разбирали в отдельном уроке: он короче, но таймаутное замирание там тоже в комплекте.
Переполнение буфера — вторая вещь, о которую я стукался: пришло 40 символов, а буфер на 24. Правило простое: лишние символы молча не сохраняются — счётчик доходит до предела и стоит, а команда, какая бы длинная ни была, обрезается до 23 знаков. Экран всё равно длиннее не показывает, так что пользователь пульта ничего не теряет, зато память платы защищена от записи мимо массива — а это уже не глюк, а порча чужих переменных.
void runCmd() { // cls — очистка if (line[0] == 'c' && line[1] == 'l' && line[2] == 's') { lcd.clear(); Serial.print("ok=cls n="); Serial.println(cmds); return; } // bl1 / bl0 — подсветка if (line[0] == 'b' && line[1] == 'l') { if (line[2] == '1') lcd.backlight(); else lcd.noBacklight(); Serial.print("ok=bl n="); Serial.println(cmds); return; } // 0:текст / 1:текст — печать в строку if ((line[0] == '0' || line[0] == '1') && line[1] == ':') { int row = 0; if (line[0] == '1') row = 1; lcd.setCursor(0, row); for (int i = 2; i < n; i++) lcd.print(line[i]); Serial.print("ok=row n="); Serial.println(cmds); return; } Serial.print("ok=unknown n="); Serial.println(cmds);}Обратите внимание на две мелочи, которые делают пульт прибором, а не поделкой. Первая — эхо-ответ: на каждую команду плата печатает ok= с типом и порядковым номером. Печатая «0:Privet» и увидев в мониторе «ok=row n=1», вы точно знаете, что доехало именно то, что отправлено, — а увидев «ok=unknown», сразу понимаете, что опечатались в команде, и не ищете глюк в дисплее. Вторая — светодиод на D12: он вспыхивает на 80 миллисекунд на каждую принятую команду. Когда пульт уедет на Bluetooth и монитора порта рядом не будет, эта вспышка останется единственным видимым подтверждением, что пакет дошёл. Пару «команда — действие — подтверждение» мы впервые собирали в уроке про управление светодиодом из Serial — здесь та же схема, только исполнитель не светодиод, а целый экран.
Пульт в симуляторе
В макетке ниже откройте монитор порта и отправляйте команды: «0:Privet mir», «1:Temp 25 C», «cls», «bl0» и обратно «bl1». Светодиод вспыхивает на каждой принятой команде, эхо ok= падает в Serial с номером по счёту. Кнопка на D2 рисует справку — шпаргалку по протоколу прямо на экране: удобно, когда пультом пользуетесь не только вы. Незнакомую строку плата честно ответит ok=unknown и экран не тронет: молчаливых отказов в этом скетче нет.
Куда этот пульт развивается дальше — и это тот редкий случай, когда следующий шаг я делал буквально на следующей неделе. Модуль HC-05, воткнутый в те же TX/RX, заменяет USB-кабель на Bluetooth: тот же самый протокол, те же команды, но пульт теперь в телефоне с терминальным приложением — схема подключения и оговорки по пинам в уроке про HC-05 и SoftwareSerial. Табло соседа работает именно так уже третий сезон. Второе направление — меню на экране: команды не печатают произвольный текст, а двигают курсор по пунктам, и это превращает пульт в интерфейс настройки; сборка такого меню с курсором — в уроке про меню и курсор на LCD. И последнее правило, которое я вынес из гаражного табло: сначала ответ, потом действие. Печатайте ok= до перерисовки экрана — тогда даже зависший lcd.clear() не оставит вас гадать, дошла ли команда вообще.
- Serial превращает LCD в табло с дистанционной сменой текста без перепрошивки: команда ушла — экран перерисовался
- Протокол держите тупо-простым: 0:текст, 1:текст, cls, bl1/bl0 — каждая хитрость формата это строки кода на плате
- Читайте посимвольно с guard Serial.available() > 0: readString() замирает на таймауте и тормозит весь loop()
- Буфер команды ограничивайте явно (24 ячейки): лишние символы молча отбрасываются, память чужая не портится
- Эхо ok= с номером и вспышка светодиода делают пульт диагностируемым: видно, что команда дошла и как понята
- 1. Почему для приёма команд выбрано посимвольное чтение вместо Serial.readString()?
- 2. Что означает ответ ok=unknown n=5 в мониторе порта?
- 3. В буфер команды пришло 40 символов, а line рассчитан на 24. Что случится?
- 4. Команда напечатана в мониторе порта, на экране ничего. Первое, что проверяем?
- 5. Зачем светодиод на D12 вспыхивает на каждую принятую команду?
Частые вопросы по теме урока
- Почему для приёма команд выбрано посимвольное чтение вместо Serial.readString()?
- readString() при пустом буфере ждёт таймаут — по умолчанию секунду замирает весь цикл. Посимвольное чтение с проверкой available() оставляет loop() живым между командами. Посимвольное чтение не блокирует loop(): таймаутного замирания нет, кнопки продолжают опрашиваться
- Что означает ответ ok=unknown n=5 в мониторе порта?
- Плата посчитала команду (n=5), светодиод мигнул, но шаблон не совпал ни с cls, ни с bl, ни с 0:/1: — значит, в команде опечатка. Экран при этом не тронут. Пятая по счёту команда дошла, но не совпала ни с одной командой протокола
- В буфер команды пришло 40 символов, а line рассчитан на 24. Что случится?
- Счётчик доходит до предела и дальше не растёт: команда обрезается до длины экрана, зато запись мимо массива невозможна. Порча соседних переменных куда дороже обрезанной надписи. Первые 23 символа сохранятся, остальные будут молча отброшены
- Команда напечатана в мониторе порта, на экране ничего. Первое, что проверяем?
- Без перевода строки команда лежит в буфере недособранной — скетч ждёт терминатор. Выпадающий список «Newline» рядом со скоростью порта — первое место диагностики любого пульта. Стоит ли перевод строки (Newline) в настройках монитора порта
- Зачем светодиод на D12 вспыхивает на каждую принятую команду?
- Когда пульт уезжает на Bluetooth, терминал не виден, а вспышка остаётся: пакет дошёл, скетч его понял. Дешёвая замена целой системе логирования. Это видимое подтверждение, что пакет дошёл и разобран — особенно когда монитора порта рядом нет