В один порт ко мне стучались двое: GPS-приёмник со строками вида $GPRMC… и служебные сообщения соседней платы с решёткой в начале — #temp=24. Вычитывать всё подряд read()-ом нельзя: первый байт уже вынут, и решить, кто пришёл, не по чему. Спасение — peek: он показывает следующий байт, не вынимая его из буфера, и решение «читать или выбросить» принимается до того, как байты потеряны.
Serial.peek — третий способ заглянуть в порт рядом с available и read: первый говорит, сколько байт ждёт, второй достаёт, третий — подсматривает. Разберём, что peek возвращает на пустом буфере, почему его нельзя складывать в char бездумно и как на нём собирается порт-сортировщик с двумя протоколами сразу.
Тройка available, read, peek: кто что делает
Механика простая до скучности, но порядок слов важен. Serial.available() возвращает число ждущих байтов; Serial.read() достаёт самый старый и двигает очередь; Serial.peek() показывает тот же самый старый, но очередь не двигает — повторный peek вернёт тот же байт, пока вы его не вычитали read-ом. На пустом буфере и read, и peek возвращают -1. Отсюда золотое правило пары: сначала available, потом peek/read — иначе сравнение с байтом на пустом месте.
Грабля самозванца: peek возвращает int, и только -1 — признак пустоты. Записав результат в char, вы превращаете -1 в байт 0xFF: пустой буфер становится неотличим от настоящего байта 255, и разбор пойдёт по неверной ветке. Держите peek в int, как в примерах ниже. Классический цикл с peek уже мелькал в уроке-калькуляторе: while (available && peek == ' ') read(); — выедает пробелы перед знаком, не трогая само слово. Тот же приём в уроке про терминал устройства чистит хвосты перед разбором команд.
Главная работа peek — маршрутизация. Пока очередь не тронута, первый байт — это ярлык всего сообщения: доллар — NMEA от GPS, решётка — служебная телеметрия соседа, цифра — команда без слов. Один просмотр — и вы либо вычитываете строку целиком, либо молча выбрасываете её до перевода строки, не потревожив чужие строки. Так на одном порту уживаются несколько говорящих, и разбор NMEA получает свой доллар, а счётчик мусора — свой остаток.
// Маршрутизация потока по первому байту.// $... — полезная строка: копим до перевода строки и печатаем// #... — служебное: молча выбрасываем до конца строки// иное — мусор: считаем по байтамconst int LED_PIN = 12;long musor = 0;long poleznoe = 0; void setup() { Serial.begin(9600); pinMode(LED_PIN, OUTPUT); Serial.println("Shlites stroki: $ - dannye, # - sluzhebnoe, ostalnoe - musor");} void loop() { while (Serial.available() > 0) { int vzglyad = Serial.peek(); // int, не char: -1 значит пусто if (vzglyad == '$') { String str = Serial.readStringUntil(10); poleznoe++; Serial.print("NMEA: "); Serial.println(str); digitalWrite(LED_PIN, HIGH); } else if (vzglyad == '#') { while (Serial.available() > 0 && Serial.read() != 10) { // выбрасываем служебное до конца строки } digitalWrite(LED_PIN, LOW); } else { Serial.read(); // мусор — по одному байту musor++; digitalWrite(LED_PIN, LOW); } }}Внимательный читатель спросит, почему служебные строки выбрасываются циклом read, а не readStringUntil. Ответ — в праве на отсутствие терминатора: если линия дрогнула и перевода строки не будет, readStringUntil продержится секунду на таймауте и вернёт хвост, а цикл с available просто вычистит всё, что есть, и пойдёт дальше. В горячем потоке секунда — это та самая копилка буфера, переполнять которую себе дороже.
Вторая тонкость — согласованность маршрутов. Пока вы копите NMEA через readStringUntil, приходящие следом байты другой строки подмешаются в ту же копилку: порт один, и очередь одна на всех. Поэтому сортировщик живёт в loop без delay: чем чаще он заглядывает, тем меньше шансов, что две строки перемешаются. Если перемешивание всё же случается — это сигнал, что скетч где-то долго стоит: место такой остановки ищут тем же способом, что и переполнение приёмного ящика в уроке про буфер.
Один нюанс из практики: peek хорош и для ловли паузы. Мой счётчик мусора рос байтами, и по его темпу я видел, что линия живёт: поток от GPS на 9600 бод — примерно 960 байт в секунду, и если за секунду не пришло ничего, разговор кончился, можно переводить стенд в сон. Арифметика байтов в секунду с ловушкой кварца 8 против 16 МГц — в уроке про скорости и рассинхрон.
Чего peek не заменяет: он не умеет ждать. В отличие от find, peek не блокируется в ожидании байта — пусто так пусто, проверяйте available и возвращайтесь на следующем проходе. Просмотр без available перед ним — половина затыков: сравнение с -1 на пустом буфере уводит разбор в ветку мусора, и честный байт, приехавший микросекундой позже, будет посчитан неверно.
| Метод | Возвращает | Двигает очередь | Пустой буфер |
|---|---|---|---|
| Serial.available() | число ждущих байтов | нет | 0 |
| Serial.read() | байт как int | да | -1 |
| Serial.peek() | байт как int | нет | -1 |
- 1. Что вернёт Serial.peek() на пустом буфере?
- 2. Чем peek отличается от read?
- 3. Что делает цикл while (available() > 0 && peek() == ' ') read();?
- 4. Почему служебные строки в сортировщике выбрасывают read-циклом, а не readStringUntil?
- Тройка порта: available — сколько ждёт, read — достать и вычесть, peek — подсмотреть и оставить
- peek и read возвращают int: -1 — пустой буфер; в char это 0xFF — самозванец, путающий пустоту с байтом 255
- peek-маршрутизация: первый байт — ярлык сообщения; доллар копим, решётку глушим, мусор считаем
- Цикл peek+read выедает пробелы и хвосты, не трогая полезный первый байт
- Сортировщик живёт без delay: чем чаще заглядываешь, тем меньше шансов перемешать строки в одной очереди
Частые вопросы по теме урока
- Что вернёт Serial.peek() на пустом буфере?
- Как и read, peek на пустом буфере возвращает int -1. Держите результат в int: char превратит -1 в байт 0xFF и спутает пустоту с настоящим байтом 255. -1
- Чем peek отличается от read?
- read достаёт байт из очереди, peek лишь показывает самый старый. Повторный peek вернёт то же самое, пока вы не вычли байт read-ом. peek показывает байт и не двигает очередь
- Что делает цикл while (available() > 0 && peek() == ' ') read();?
- Классика из калькулятора: peek смотрит, read вычитает — и так по кругу, пока впереди пробелы. Первое слово остаётся лежать нетронутым. выедает пробелы подряд, не трогая первый непробельный байт
- Почему служебные строки в сортировщике выбрасывают read-циклом, а не readStringUntil?
- Без терминатора readStringUntil стоит секунду и возвращает хвост. Цикл available/read вычищает только то, что есть, — горячий поток не ждёт. у read-цикла нет секундного таймаута: он не зависнет на строке без терминатора