Команда «мигни дважды» не срабатывала третий раз подряд, и я уже успел согреться на новой среде: в поле монитора всё выглядело безупречно, плата молчала. Через двадцать минут эхо показало правду — мои «2» и Enter уезжали тремя байтами, а скетч ждал четвёртый. Разрыв между тем, что вы видите в поле ввода, и тем, что реально доезжает по проводу, — главный источник недоразумений с монитором порта, и он же — лучшая тема для первого знакомства с серией на байтовом уровне.
Панель монитора, её кнопки и тайм-метки интерфейса разобраны в уроке про монитор порта в IDE 2, там же — база приёма и отправки. Здесь — именно входящая сторона: что происходит с набранным текстом между Enter и скетчем. Разбор веду с кодами и числами, потому что байты не врут: символ «2» — это код 50, и как только рука привыкает читать эхо, спор о том, кто виноват, заканчивается за минуту.
Три байта и два хвоста
Печатаете «12» и жмёте Enter — в порт уходят два байта со значениями 49 и 50, а дальше решает список окончаний строки. No line ending: на этом всё, два байта. NL — добавляется код 10, CR — код 13, Both NL & CR — сначала 13, потом 10, именно в таком порядке, я проверял на эхо не один раз. Судьба парсера зависит от этих хвостов целиком: Serial.parseInt пропустит всё, что не цифра, и переживёт любой из четырёх вариантов; readStringUntil ждёт код 10 как терминал и без него молчит; собственный посимвольный сборщик видит хвосты как отдельные байты. Один и тот же ввод — три разных судьбы, о чём я считал байты и разбирал парсеры в уроке про окончания строки и parseInt.
Хвост, который остался в буфере, бьёт по второму проходу loop: parseInt съел число, коды 13 и 10 остались, available() снова больше нуля, и парсер честно возвращает ноль — вы получаете лишнюю команду с аргументом 0. Лечение однородное: после разбора вычитывать буфер до дна, while (Serial.available() > 0) { Serial.read(); }. Буфер приёма у Uno — 64 байта: если вы вставили в поле длинную строку и никто её не читает, всё сверх шестидесяти четырёх теряется молча, без признаков на стороне среды. Для ручного ввода это потолок, о котором стоит помнить, когда отправляете плате конфигурацию из десяти строк за один раз.
Коды символов — таблица, которую стоит носить в голове, она короткая: цифры занимают коды с 48 по 57, заглавные латинские буквы — с 65, строчные — с 97, пробел — 32, хвосты строки — 13 и 10. Из этого следует занятная деталь: текст «12» и число двенадцать — разные сущности, и пока парсер не собрал из кодов 49 и 50 значение, плата видит два чужих байта. Когда коллега присылает «смотрите, всё написано правильно», я прошу вывод эха: скриншот с kod=49, kod=50, kod=10 закрывает спор надёжнее любых слов — и именно поэтому шаблон эха ниже я держу в заготовках под рукой.
Пара хвостов не случайна: у кодов 13 и 10 отдельные биографии, и обе начинаются у печатной машинки. CR — возврат каретки: каретку отводили в начало строки; LF — перевод строки: бумага поднималась на одну позицию вверх. Телетайпы унаследовали оба движения, а дальше истории разошлись — одни системы заканчивают строки парой 13/10 до сих пор, другие обходятся одним LF. Список окончаний в мониторе — последний звонок той эпохи, и Both NL & CR существует потому, что кому-то в какой-то терминал понадобился полный набор.
Эхо — восьмистрочный скетч, который закрывает девять из десяти таких историй: читаем всё, что есть, печатаем код каждого байта назад. Цифра «2» покажет 50, буква «a» — 97, а хвост строки — 13, 10 или ничего, смотря что выбрано в списке. Наглядная таблица кодов живёт в уроке про Serial.available и read; здесь важен сам приём: не гадать, а печатать. Эхо пригодится и шире личного поля ввода: Bluetooth-модуль, USB-TTL переходник, чужой терминал — любой новый сосед по порту сначала проходит через восемь строк эха, и только потом получает право на скетч с логикой. Время диагностики падает с получаса до одной отправки — цена вопроса восемь строк, которые я держу в заготовках под рукой.
Заодно спокойнее про архитектуру: TX и RX — два отдельных провода, плата умеет печатать и слушать одновременно, никакого занятого эфира у порта нет. Отсюда диагностический вывод, экономящий часы: если в ответ на команду в мониторе тишина, порт почти никогда не глохнет — просто скетч не печатает. Смотрите не на кабель, а на ветки условия: добралась ли команда до парсера и стоит ли в той ветке println.
void setup() { Serial.begin(9600); Serial.println("ehho gotovo: pechatayu kody vsex bajtov");} void loop() { if (Serial.available() > 0) { int c = Serial.read(); Serial.print("kod="); Serial.println(c); // cifra 1 -> 49, 2 -> 50; NL -> 10, CR -> 13 }}| Выбор в списке | Что уходит за строкой «12» | Кто выживет |
|---|---|---|
| No line ending | 49, 50 | parseInt — да; readStringUntil — нет |
| NL | 49, 50, 10 | все парсеры |
| CR | 49, 50, 13 | parseInt — да; readStringUntil молчит |
| Both NL & CR | 49, 50, 13, 10 | все, но посимвольщик увидит два хвоста |
Скорость как договор и метки как секундомер
Скорость в мониторе — вторая половина договора, первую подписывает Serial.begin: числа обязаны совпасть, иначе биты едут мимо разметки и вместо строк — мусор. Правило я держу из урока про выбор платы и порта, но с оговоркой, которую многие пропускают: на платах с USB-CDC (Leonardo, свежие ESP32 с нативным USB) цифра скорости — ритуал, шина между ними и компьютером своя, и хоть 300 поставьте — текст не испортится. На классических Uno и Nano с CH340 всё наоборот: несовпадение гарантированно ломает вывод. Для ввода команд руками 9600 хватает с запасом; 115200 нужны потокам показаний — арифметику кадров я считал в уроке про плоттер как осциллограф.
Метки времени ставит среда в момент приёма строки компьютером, отсчёт — от открытия панели; открытие дёргает DTR и ресетит плату, поэтому метки почти совпадают с возрастом скетча, но «почти» — с оговоркой в десятки-сотни миллисекунд. Полезнее другого: разность соседних меток — секундомер на стороне ПК. Печатаете пару раз в 300 мс — метки подтверждают период; строка опоздала на две секунды — loop где-то стоял, и искать надо delay или медленную операцию между принтами. Честная граница: метка показывает приём, а не отправку, и очередь USB вносит свою мелочь — для интервалов длиннее сотни миллисекунд погрешность несущественна, для микросекундных замеров нужен micros() внутри скетча.
Выбор парсера — это выбор протокола, и он стоит минуту раздумий на старте проекта. parseInt — когда плате нужны числа: быстрый, терпимый к хвостам, но возвращает ноль на пустоте и ждёт секунду. readStringUntil — когда нужны слова: точный терминал по коду 10, но молчит без NL и съедает память под строку. Посимвольный сборщик — когда в системе и команды, и поток: без таймаутов, с полным контролем над хвостами. Разбор всех троих на общих примерах — в уроке про числа через parseInt, моя личная привычка после истории с молчащим пультом — всегда начинать с эха и только потом прикручивать парсер.
И мост дальше по кластеру: те же строки, которые вы отправляете плате, работают и в обратную сторону. Пара имя=значение, напечатанная скетчем, читается фильтром монитора и превращается плоттером в подписанный ряд — об этом второй урок, где разберём, как среда делит строку на серии и что делают с масштабом.
Про соседей по порту. Монитор в IDE 2 — не единственный, кто может открыть COM: PuTTY, minicom, консольная утилита производителя платы претендуют на тот же порт, и среда честно сообщит, что порт занят, — а вы будете искать виноватого в скетче. Правило простое: один владелец на порт; запускаете внешний терминал — закрывайте панель монитора, и наоборот. Отдельная мелочь: заливка тоже открывает порт на несколько секунд, и если в этот момент порт держит кто-то ещё, заливка спотыкается без внятного объяснения — сначала закройте всё, что могло прицепиться к COM, потом ищите дальше.
И граница применимости самого монитора: он текстовый, и руками через его поле не набрать ни нулевой байт, ни произвольный код вроде 0x00 — только печатаемые символы. Для двоичных протоколов нужна пара: скетч с Serial.write и внешний терминал с hex-режимом; монитор в этой игре остаётся инструментом текста, и втискивать в него бинарный обмен так же бесперспективно, как править фотографию в блокноте.
- 1. Вы отправили «12» с окончанием NL. Сколько байтов уехало в порт?
- 2. Выбрано Both NL & CR. В каком порядке уйдут хвосты?
- 3. В буфере остались только окончания строки, скетч зовёт Serial.parseInt(). Что он вернёт?
- 4. readStringUntil молчит, хотя строки уходят. Первое действие?
- 5. Разность соседних timestamp-меток показывает…
- «12» — это байты 49 и 50; окончание строки добавляет 10, 13 или оба, и от этого выбора зависит выживание парсера.
- Both NL & CR пишет 13, потом 10; readStringUntil без кода 10 молчит, parseInt переживает любой вариант.
- После parseInt вычитывайте буфер до дна: хвосты возвращаются на следующем проходе как ложная команда с нулём.
- Буфер приёма Uno — 64 байта; непрочитанное сверх — теряется молча.
- Разность timestamp-меток — секундомер на стороне ПК; на USB-CDC платах цифра скорости — ритуал, на CH340 — жизнь и смерть вывода.
Частые вопросы по теме урока
- Вы отправили «12» с окончанием NL. Сколько байтов уехало в порт?
- Две цифры — коды 49 и 50 — плюс терминал NL с кодом 10. Скорость тут ни при чём: она влияет на темп, а не на состав байтов. три
- Выбрано Both NL & CR. В каком порядке уйдут хвосты?
- Среда пишет сначала возврат каретки (13), затем перевод строки (10). Посимвольный парсер увидит два хвостовых байта подряд. 13, потом 10
- В буфере остались только окончания строки, скетч зовёт Serial.parseInt(). Что он вернёт?
- parseInt пропускает не-цифры и, не найдя числа, отдаёт ноль — поэтому ветка if (cmd == 0) внезапно выполняется после каждой команды. 0
- readStringUntil молчит, хотя строки уходят. Первое действие?
- readStringUntil ждёт код 10 как терминал. Без NL строка копится в буфере и не отдаётся — достаточно выбрать NL или Both NL & CR. поставить в списке окончаний NL
- Разность соседних timestamp-меток показывает…
- Метки ставит среда в момент приёма. Разность соседних — секундомер на стороне ПК: удлинение интервала подсказывает, где loop стоял. интервал между приёмами строк на стороне компьютера