Автоматическая поливалка на Arduino довела меня до белого каления: раз в пять замеров датчик влажности показывал ровный ноль, будто провод выдернули. Я два вечера прощупывал мультиметром контакты и перепаивал разъёмы — безрезультатно. Спасла банальная печать в порт. Я добавил в loop() одну строку с меткой времени и значением, запустил на десять минут и увидел закономерность: ноль приходил всегда через полсекунды после старта помпы. Помпа создавала помехи на аналоговом входе — четыре часа поисков, а виновник был виден в логе с первого раза. С тех пор мой девиз: печатаю состояние — вижу, где сломалось. Serial в отладке сильнее мультиметра, потому что показывает не только напряжение, но и логику: что, когда и в каком порядке подумала ваша программа.
Разберём три навыка. Первый — форматированный вывод: одна строка — одно событие, аккуратные пары «имя=значение» и CSV-формат, который сразу рисуется в Serial Plotter. Второй — печать переменных через Serial.print() в правильных местах: техника «print до и после» сужает поиск ошибки до конкретной строки кода. Третий — метки millis(): как превратить разрозненные события во временную шкалу, измерить длительность любой операции и поймать зависание с помощью тайм-аута. Всё это работает без осциллографа, логического анализатора и паяльника — только монитор порта и пара строк кода в нужных местах скетча.
Одна строка — одно событие: вывод, который читается
Главное правило отладочного вывода: одна итерация loop() — одна строка. Если печатать вразнобой, из разных мест и без переводов строки, через минуту вы получите кашу, в которой ничего не найти. Удобный шаблон — пары «имя=значение» через разделитель: t=12 raw=512 lvl=127. Для графика тот же вывод переписывают в CSV-формат: числа через запятую, первая строка — заголовок столбцов. Такой лог открывается в редакторе таблиц как файл, а встроенный Serial Plotter (иконка лупы с графиком в правом верхнем углу Arduino IDE) рисует кривые по числовым строкам без каких-либо настроек: сколько чисел в строке — столько кривых на экране. Заголовок печатается один раз в setup(), данные — каждый цикл в loop(): получатся аккуратные столбцы «время, сигнал, результат», которые можно сравнивать между запусками.
Разделитель выбирайте под задачу: два пробела или табуляция — для человеческих глаз в мониторе, запятая — для машин: графиков и таблиц. Скромный предел здравого смысла — три-четыре значения в строке: больше — и монитор превращается в стену цифр. Если цикл быстрый, не печатайте на каждом обороте: печатайте раз в десять проходов или раз в двести миллисекунд через millis(), иначе порт станет узким горлышком всей программы. Код ниже раз в полсекунды выводит время в миллисекундах, сырые единицы аналого-цифрового преобразователя (0–1023) и ту же величину, пересчитанную через map() в проценты. Скопируйте такой лог в текстовый файл — и вы сможете построить график показаний датчика хоть в редакторе таблиц, хоть от руки на бумажке.
void setup() { Serial.begin(9600); Serial.println("time_ms,raw,percent"); // заголовок печатаем один раз} void loop() { int raw = analogRead(A0); int percent = map(raw, 0, 1023, 0, 100); Serial.print(millis()); Serial.print(","); Serial.print(raw); Serial.print(","); Serial.println(percent); delay(500);}Печать до и после: где именно сломалось
Когда поведение программы расходится с ожиданиями, помогает метод «вилки». Вы подозреваете, что ошибка где-то между строкой A и строкой B, — и печатаете в обеих точках. Пришло «A», но не пришло «B»? Ошибка внутри, и диапазон сужается вдвое. Три-четыре таких деления — и вы смотрите на конкретную строку, где логика сворачивает не туда. Особенно важно проверять входы в условия: печатайте значение, из-за которого if принял решение. Я не раз терял часы на том, что переменная оказывалась не 0, а 1, или сравнение шло не с тем типом. Состояния при этом называйте словами: GORIT и NE GORIT читаются в логе мгновенно, а «1» и «0» через час работы превращаются в загадку. Для этого хватает if/else перед печатью — printf на Uno нет, но цепочка print-ов заменяет его целиком.
Печатайте и то, что НЕ произошло: ветка else с сообщением «условие не выполнилось, значение равно N» часто говорит больше, чем сотня подтверждений. Для времени используйте тип unsigned long без исключений — обычный int переполнится уже через полминуты, и ваши метки поедут в минус. Не оставляйте подробный отладочный вывод в финальной прошивке: он тормозит программу (об этом — предупреждение ниже) и засоряет канал, если плата общается с чем-то ещё. Мой формат — две ступени: во время разработки лог подробный, перед «сдачей проекта» остаются три-четыре ключевые строки о важных событиях. Код ниже — мигание без delay с журналом: обратите внимание, что loop() не блокируется, а каждая смена состояния печатается одной строкой с меткой времени.
int ledState = LOW;unsigned long lastChange = 0;const unsigned long INTERVAL = 1000; void setup() { Serial.begin(9600); pinMode(13, OUTPUT);} void loop() { unsigned long now = millis(); if (now - lastChange >= INTERVAL) { lastChange = now; if (ledState == LOW) { ledState = HIGH; } else { ledState = LOW; } digitalWrite(13, ledState); Serial.print("t="); Serial.print(now); Serial.print(" sostoyanie: "); if (ledState == HIGH) { Serial.println("GORIT"); } else { Serial.println("NE GORIT"); } }}Метки millis() и тайм-ауты против зависаний
Метки millis() превращают лог из набора строк в хронологию. Когда вы видите «t=1042 событие, t=1090 событие, t=5000 тишина», вывод напрашивается сам: между 1090 и 5000 программа застряла — а дальше сужаете круг тем же методом вилки. Классическая причина зависаний — бесконечное ожидание: while (analogRead(A0) > 300) без ограничения по времени будет крутиться вечно, если провод отвалился или датчик умер. Лекарство — тайм-аут прямо в условии цикла: ждём, но не дольше пяти секунд, после чего честно сообщаем о проблеме и продолжаем работу. Такой сторожевой таймер отличает прошивку «для балкона» от прошивки «для производства». Второй подарок меток — измерение длительностей: засекли millis() до и после операции, вычли — узнали, сколько занимает чтение датчика или вывод на дисплей.
Осталась пара приёмов из копилки. Объяснение кода вслух — хоть коту, хоть резиновой утке — находит ошибку раньше, чем печать: вы вынуждены проговаривать каждое условие, и нестыковка всплывает на третьем предложении. Не пытайтесь логировать всё подряд — печатайте события: старт, переход состояния, ошибку, выполнение редкой ветки; десять событий на экране информативнее тысячи одинаковых строк. И финальный совет: найдя причину, чините корень, а не симптом. Если помпа глушит аналоговый вход — ставьте конденсатор и разносите питание, а не добавляйте в код «если ноль — пропустить измерение». Лог — это фонарик, который показывает, где болит, а не обезболивающее.
unsigned long t0 = 0; void setup() { Serial.begin(9600); pinMode(A0, INPUT);} void loop() { if (analogRead(A0) > 500) { t0 = millis(); Serial.print("[t="); Serial.print(t0); Serial.println("] Signal vysokiy, zhdem spada"); while (analogRead(A0) > 300 && millis() - t0 < 5000) { // просто ждём, но не дольше 5 секунд } if (analogRead(A0) > 300) { Serial.println("[!] Za 5 s signal ne upal. Proveryay cep na A0."); } else { Serial.print("[t="); Serial.print(millis()); Serial.print("] Spad. Dlitelnost: "); Serial.print(millis() - t0); Serial.println(" ms"); } } delay(200);}- Одна итерация loop() — одна строка лога: пары «имя=значение» или CSV через запятую
- Заголовок столбцов печатайте один раз в setup(), данные — в loop()
- Метод «вилки»: print до и после подозрительного кода сужает поиск до одной строки
- Метки millis() превращают лог в хронологию и позволяют ставить тайм-ауты против зависаний
- Serial небесплатен: на 9600 бод каждый символ — около 1 мс, не спамьте в быстрых циклах
- 1. Зачем печатать millis() рядом с событием?
- 2. Какой вывод удобнее всего для Serial Plotter?
- 3. Программа «виснет» где-то между двумя блоками кода. С чего начать?
- 4. Почему с большим количеством Serial.print цикл может работать иначе, чем без него?
На этом разговор с платой через порт завершён — у вас полный цикл: плата печатает данные, слушает команды, а вы умеете заглянуть в любую программу через аккуратный лог. Дальше по самоучителю пойдут разделы о датчиках, дисплеях и хранении данных, и в каждом Serial останется вашим личным микроскопом: подключили новый модуль — первым делом напечатайте, что он отвечает. Не выкидывайте и навык интерактивности: команды с клавиатуры превращают любой проект в лабораторный стенд, где пороги и режимы меняются на лету. Прямо сейчас сделайте простое: возьмите финальный скетч любого прошлого урока и добавьте в него CSV-лог с метками millis(). Пять минут работы — и вы увидите свои программы так, как не видели раньше.
Частые вопросы по теме урока
- Зачем печатать millis() рядом с событием?
- Метка времени превращает разрозненные строки в хронологию: сразу видно, что шло до чего и какие были интервалы между событиями. Чтобы видеть, когда и в каком порядке происходят события
- Какой вывод удобнее всего для Serial Plotter?
- Plotter строит по одной точке на каждое число в строке; CSV-строка «время,значение» рисуется как готовая кривая без дополнительных настроек. Числа через запятую, одна строка — один набор значений
- Программа «виснет» где-то между двумя блоками кода. С чего начать?
- Печать-«вилка» показывает, до какой точки выполнение доходит. Сужая диапазон вдвое за шаг, вы находите строку, где логика ломается. Поставить print до и после подозрительного участка
- Почему с большим количеством Serial.print цикл может работать иначе, чем без него?
- UART физически передаёт биты с конечной скоростью: строка в 50 символов на 9600 бод — это примерно 50 мс паузы внутри loop(). Передача каждого символа занимает время — около 1 мс на 9600 бод