Скетч поливальника вёл себя как капризный кот: на столе срабатывал, на окне молчал. Вечер я потратил классически — переставил провода наугад, переписал условие, перезалил скетч ещё раз. Ни один шаг не приблизил к причине, потому что я менял всё сразу и не знал, какая ступень сломана: логика, печать на порт или электрика. Каждый перезапуск с новой версией врал о предыдущей — к полуночи у меня было четыре скетча, и ни один ничего не доказывал. На следующий день я сел с планом, и виновника нашли за десять минут: окислившаяся перемычка в земле макетки, двести ом между платой и рейкой.
План стоило бы выучить раньше. Пирамида из трёх ступеней: сначала логика в симуляторе — там плата всегда под рукой, время послушно, провода не гнилые; затем Serial-точки и половинное деление кода; и только потом мультиметр на живую плату. Ступени идут в порядке роста цены: симулятор бесплатен и мгновенен, Serial-печать замедляет скетч на миллисекунды за строку, щупы требуют платы, стола и терпения. Мой ночной провал был нарушением порядка: я начал с третьей ступени, не проверив первую. Методика — единственный инструмент, который переносится с проекта на проект целиком, поэтому час на неё возвращается дольше всего.
Ступень первая: логика в симуляторе
Симулятор сайта — та же Uno с макеткой в браузере: светодиоды зажигаются, кнопки нажимаются, ползунки крутятся, Serial-монитор печатает. Ценность не в картинке, а в управляемости: время замедляется, вход держится ровно на границе порога, пограничный случай повторяется двадцать раз подряд — на живой плате такое терпение не водится. Логические промахи — не та ветка if, не тот знак сравнения, перепутанный порядок фаз — выявляются здесь за минуты, без единого провода и без риска спалить деталь. Приём для таймингов: замедлите время и посмотрите на фазы построчно — на плате вы бы фотографировали светодиод и надеялись поймать кадр. Граница «ровно на пороге» в симе держится ползунком столько, сколько нужно; на живом датчике она живёт доли секунды. В симе нельзя спалить детали, поэтому смелые гипотезы — нулевые периоды, деление на ноль, зеркальные фазы — проверяйте именно там: живая плата такие эксперименты не прощает.
Границы честные: дребезг контакта, просевшее питание, наводка на длинный провод в симуляторе не воспроизводятся — движок рисует идеальную электрику. Отсюда правило ступени: пока логика не сходится в симе, плату даже не включайте. Когда в симе всё ровно, а железо молчит, вы точно знаете, что виновник ниже — на ступенях с Serial-точками и щупами. Стенды, которые доступны в браузере помимо нашего, и их отличия я собирал в уроке про Wokwi и компанию.
Ступень вторая: Serial-точки и половинное деление
Serial-точка — строка вида etap1 val=512, напечатанная при входе в ветку. Метод: расставить метки на каждом развороте скетча, открыть монитор порта и найти первое место, где реальность разошлась с планом. Строка не пришла — ветка мертва, ищите выше условие; строка пришла со странным значением — виноваты значения, а не поток. Тонкость, которую все замечают на второй день: печать стоит времени. На 9600 бод канал тянет примерно 960 байт в секунду — байт с битами старта и стопа занимает около миллисекунды, значит строка из двадцати символов съедает двадцать миллисекунд; на 115200 тот же текст проходит за две. Если период мигалки 300 мс, печать на каждом витке loop() заберёт заметный кусок — метки ставят на события, а не на каждый проход. Формат метки выбирайте машинно-читаемым: пары имя=значение через пробел потом распарсятся parseInt() или поиском в блокноте, а фраза «сейчас проверяю датчик» парсится только глазами.
Половинное деление — вторая половина ступени, когда меток уже не хватает. Закомментируйте половину кода — или оберните её в #if 0, потому что блочные комментарии вкладывать нельзя, — залейте, посмотрите на симптом. Пропал: виновник внутри закрытой части. Остался: смотрите в оставшейся. Три-четыре среза локализуют строку в файле на сотню строк: сто строк, пятьдесят, двадцать пять, двенадцать. Скучно до зевоты и работает даже тогда, когда кажется, что ломается всё сразу. Правило гигиены среза: закрытую половину держите одним блоком с пометкой, почему она выключена, — иначе через час вы не найдёте, что именно отключили, и включите обратно всё подряд.
Границы метода жёсткие. Печать внутри обработчика прерывания запрещена: Serial держится на прерываниях и зависает внутри ISR — рабочий приём там другой, флаг в volatile-переменной, о чём я писал в уроке про volatile и прерывания. И второй нюанс: Serial.flush() в современных ядрах не чистит приёмник, а дожидается отправки — чтобы слить вход, читайте read() в цикле. Парсинг значений и протоколы с метками разобраны глубже в уроке про отладку через Serial; здесь же я про порядок всей пирамиды.
| Симптом | Первая ступень для проверки | Кто чаще виноват |
|---|---|---|
| В симе работает, на плате нет | мультиметр: рейки под нагрузкой и прозвонка | гнилой контакт на макетке, просевшие 5 В |
| Скетч залипает после кнопки | метка до и после чтения пина | дребезг: фронт насчитан по десять раз |
| Монитор молчит при живом скетче | скорость монитора против скорости в скетче | 9600 в коде, 115200 в мониторе |
| Датчик выдаёт пляшущие значения | печать сырых замеров раз в секунду | висящий вход, наводка, дрожь питания |
| Ветка if не срабатывает никогда | метка внутри ветки и печать обоих операндов | не тот знак сравнения или переполнение типа |
Ступень третья: мультиметр на живой плате
Мультиметр входит в дело, когда логика в симе сходится, метки печатают честные значения, а схема всё равно молчит. Три измерения закрывают большинство полевых случаев. Первое — напряжение на рейках +5 В и GND под нагрузкой: просело до 4,4 В — источник не тянет, причём без нагрузки тот же блок показывал честные пять. Второе — прозвонка подозрительной дорожки: писк пропал — контакт гнилой. Третье — падение напряжения на проводе под током: доли вольта на перемычке — она и есть виновник. Куда какие щупы и что значат цифры — отдельный урок про мультиметр; здесь важен порядок, а не кнопки прибора. Мой порядок фиксированный: сначала питание на рейках — оно объясняет половину странностей сразу; затем земля от платы до макетки — вторая половина; и только потом сигнальные провода по одному. Перемычка стоит копейки, а часы на её поиски стоят дороже.
Порядок экономит часы, и ночная история из первого абзаца кончилась по учебнику. В симе поливальник работал идеально, Serial-метки печатали правильные значения, а прозвонка показала двести ом между землёй платы и землёй макетки — окислившаяся перемычка, которую писк уже не услышал бы, а вольтметр показал сразу. Десять секунд прибора против вечера перестановки проводов: мультиметр не увидит «не тот знак сравнения», зато Serial не увидит гнилую землю — у каждой ступени своя территория, и они не заменяют друг друга. С тех пор перемычки в моём ящике делятся на проверенные и подозрительные, и вторые в макетках не участвуют принципиально.
// Ночник с Serial-точками: метки на каждом развороте.// Пока датчика нет, его роль играет потенциометр на A0. const int PIN_SENSOR = A0;const int PIN_LED = 12;const int POROG = 600; // 600 из 1023: включаем при ощутимой темноте void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(9600); Serial.println("Nochnik: tochki etap1..etap3");} void loop() { int val = analogRead(PIN_SENSOR); Serial.print("etap1 val="); // метка 1: датчик прочитан Serial.println(val); int temno = (val > POROG) ? 1 : 0; Serial.print("etap2 temno="); // метка 2: сравнение сделано Serial.println(temno); if (temno == 1) { digitalWrite(PIN_LED, HIGH); Serial.println("etap3 svet=ON"); // метка 3: ветка доехала до конца } else { digitalWrite(PIN_LED, LOW); } delay(200); // 5 строк в секунду — монитор читаем}Дефект в скетче выше — pinMode, направленная на другой пин, — по монитору не видна: все три метки печатают уверенные строки. Противоречие собирается из пары «монитор против светодиода», и половинное деление доводит до строки setup() за три среза: закрыли ветку — симптом остался, закрыли метки — остался, остался setup() — а там уже и искать нечего. На живой плате сценарий продолжается мультиметром: если метки сходятся, а лампа молчит, щуп идёт на пин, потом на рейку. Заметьте и другую сторону: монитор остаётся честным историком — строки печатались в реальном времени, и по ним видно, когда именно лампа должна была зажечься. Цифры при этом умеют скрывать дрожь порога — кривая на плоттере показывает её честнее, об этом в уроке про Serial Plotter.
- Пирамида: симулятор для логики, Serial-метки для потока выполнения, мультиметр для электрики — в порядке роста цены ступени
- Serial-точка — строка имя=значение при входе в ветку: не пришла — ветка мертва, пришла со странным значением — виноваты значения
- 9600 бод тянет примерно 960 байт/с: строка из 20 символов стоит около 20 мс — метки на события, не на каждый виток loop()
- Половинное деление: закомментировали половину или обернули в #if 0 — три-четыре среза находят строку в сотне
- Печать внутри ISR запрещена: Serial держится на прерываниях; в обработчике — только volatile-флаг, печать в loop()
- 1. Логика в симуляторе сходится, на живой плате не работает. С чего начать?
- 2. Сколько времени занимает печать строки из 20 символов на 9600 бод?
- 3. Почему нельзя вызывать Serial.print внутри обработчика прерывания?
- 4. Половинное деление: закрыли половину кода — симптом остался. Вывод?
- 5. Serial.flush() в современных ядрах Arduino делает что?
Частые вопросы по теме урока
- Логика в симуляторе сходится, на живой плате не работает. С чего начать?
- Когда логика сходится, а железо молчит, виновата электрика: просевшее питание и гнилые контакты. Симулятор идеальную электрику не рисует и такие промахи не покажет. мультиметр: рейки питания под нагрузкой и прозвонка контактов
- Сколько времени занимает печать строки из 20 символов на 9600 бод?
- 9600 бод — примерно 960 байт в секунду, байт с битами старта и стопа стоит около миллисекунды. Двадцать символов — двадцать миллисекунд; на 115200 было бы около двух. около 20 мс
- Почему нельзя вызывать Serial.print внутри обработчика прерывания?
- Печать полагается на прерывания, а внутри ISR они выключены — ждущий механизм никогда не дождётся. В обработчике живёт только volatile-флаг. Serial держится на прерываниях, которые внутри ISR запрещены, — печать зависает
- Половинное деление: закрыли половину кода — симптом остался. Вывод?
- Симптом живёт там, что осталось выполняться. Следующий срез делит не закрытую часть; три-четыре деления выводят на конкретную строку. виновник в оставшейся половине — режьте её
- Serial.flush() в современных ядрах Arduino делает что?
- После ядра 1.0 flush ждёт окончания отправки, а не чистит приёмник. Чтобы слить вход, читают read() в цикле до пустого available(). дожидается, пока все байты уйдут из буфера передачи