Метеостанция на балконе отработала четыре дня и замерла в 07:42 — время я знаю до минуты, столько и показывали часы на экране, застывшие навсегда. Плата дышала: светодиод питания горел, но Serial не отвечал даже после переподключения монитора. Помогало одно — передёрнуть USB. Через неделю таких поездок с табуреткой к окну я понял, что лечу не болезнь, а симптом.
Код выглядел безупречно: Wire.requestFrom, вычитывание буфера, вывод на экран. Виновником оказалось бесконечное ожидание — самая старая беда шины I2C. В этом уроке разберём, где именно подвисает библиотека Wire, как поставить обмену дедлайн через Wire.setWireTimeout и как реанимировать шину, когда слейв вцепился в линию мёртвой хваткой. А заодно соберём диагност, который по каждому адресу печатает «найдено» или «нет» — и никогда не крутится вечно. Кстати, передёргивание USB — это перезагрузка платы, и она не чинит шину: мастер перезапустился, а слейв-то про это не знает.
Где именно виснет: две функции-ловушки
Первая ловушка — сама библиотека. Wire.requestFrom() и Wire.endTransmission() блокирующие: на Uno мастер-контроллер TWI ждёт флагов железа, пока обмен не завершится. В норме это быстро — на 100 кГц один байт проходит за 90 микросекунд (девять битов по десять), транзакция из пары байтов укладывается в доли миллисекунды. Но если слейв держит линию или провода в плохом контакте, ждать можно вечно: форумы про «скетч замер в Wire.requestFrom» хватит на многотомник. Мой ориентир: транзакция, не уложившаяся в 50 мс, — это уже не обмен, а застревание. В смежном промышленном стандарте SMBus лимит на удержание линии слейвом и вовсе 35 мс.
Вторая ловушка — наша собственная. Схема while (!Wire.available()) без запроса — вечный цикл: буфер приёма пуст, пополняться ему не из чего, и available() не вырастет никогда. Таких циклов я насмотрелся в чужих скетчах, пока разбирал зависшие платы. У любого ожидания на шине должен быть дедлайн — и лучше, чтобы его ставил скетч, а не табуретка у окна.
| Симптом | Что происходит | Первая помощь |
|---|---|---|
| Зависает сразу после перепрошивки | Слейв застрял посреди транзакции и держит SDA: мастера сбросили на середине байта | Тактовая реанимация: до девяти импульсов на SCL |
| Виснет через часы или дни | Dupont-контакт надломлен, шина ловит наводку | Короткие цельные провода, подтяжка 2,2 кОм |
| Виснет на первом же обращении | Перепутаны SDA и SCL или нет общей земли | Сканер адресов и осмотр проводки |
| Подвисает при получении данных | В Wire.onReceive() затесался delay() или Serial | В обработчике только переложить байты и вернуться |
Теперь моя любимая история-тупик. Неделю я воевал с метеостанцией: переносил delay, переписывал loop, перезаливал скетч с другими таймингами. А механика виснущей шины была проста. Во время одной из перепрошивок мастер сбросился ровно посреди транзакции: слейв уже ответил на адрес, прижал SDA и ждёт оставшихся тактов, которых больше не будет. Новый скетч честно вызывает Wire.begin(), но шина занята: старт-условие не сгенерировать, пока линия прижата. Сброс мастера не чинит шину — слейв не умеет узнавать, что напарник перезагрузился.
Отсюда правило, которое я вывел на своих граблях: зависание сразу после перепрошивки — почти всегда слейв с прижатой линией, а не ваш код. Проверяется за минуту мультиметром: SDA или SCL сидят ниже вольта при всех отпущенных линиях — держит слейв. Отдельная глава тех же граблей — опрос слейва чаще, чем он успевает думать, о чём писал в уроке про две платы на одной шине: там виснет не Wire, а логика, но симптомы перепутать легко.
Таймауты: дедлайн для Wire
С начала 2021 года (ядро AVR 1.8.3) у библиотеки Wire есть штатный выход: Wire.setWireTimeout() в setup(). Первый аргумент — предел в микросекундах, второй — приказ сбросить контроллер шины при срабатывании. Я ставлю Wire.setWireTimeout(25000, true): 25 мс — в двести с лишним раз длиннее нормального байта, но несравнимо короче «навсегда». Когда транзакция застревает, библиотека прерывает её, перезапускает TWI-железо, а флаг Wire.getWireTimeoutFlag() докладывает о случившемся — его полезно читать и печатать в журнал. На ESP32 и ESP8266 заботиться меньше: там у Wire есть свой встроенный таймаут, по умолчанию около 50 мс.
Если ядро вашей платы старое и компилятор не знает setWireTimeout(), стройте дедлайн вручную на millis() — приём тот же, что в уроке про жизнь без delay. А ещё пригодится сканер адресов: прежде чем лечить шину таймаутами, убедитесь, что устройства вообще отвечают и код возврата endTransmission() не намекает на перепутанные провода. Полный диагностический скетч для настоящей платы — ниже: и таймаут библиотеки, и свой дедлайн на вычитывание, и замер времени каждой проверки.
#include <Wire.h> byte targets[2] = {0x27, 0x68}; // LCD-переходник и часы DS3231 void probe(byte addr) { unsigned long t0 = micros(); Wire.beginTransmission(addr); byte code = Wire.endTransmission(); // 0 — устройство ответило if (code == 0) { Wire.requestFrom(addr, 1); // просим контрольный байт unsigned long deadline = millis() + 100; while (Wire.available() < 1 && millis() < deadline) { delay(1); // ждём, но не вечно } if (Wire.available() >= 1) { byte b = Wire.read(); Serial.print("adr=0x"); Serial.print(addr, HEX); Serial.print(" naiden=1 bayt="); Serial.println(b); } else { Serial.print("adr=0x"); Serial.print(addr, HEX); Serial.println(" naiden=0 (таймаут, байта нет)"); } } else { Serial.print("adr=0x"); Serial.print(addr, HEX); Serial.print(" naiden=0 code="); Serial.println(code); } Serial.print("proverka_mks="); Serial.println(micros() - t0);} void setup() { Wire.begin(); Wire.setWireTimeout(25000, true); // 25 мс — и свобода Serial.begin(9600); Serial.println("Diagnost I2C: vechnyh ozhidanij net");} void loop() { for (byte i = 0; i < 2; i++) probe(targets[i]); if (Wire.getWireTimeoutFlag()) { Serial.println("flag=1 (сработал таймаут библиотеки)"); Wire.clearWireTimeoutFlag(); } Serial.println("---"); delay(3000);}Реанимация: девять тактов по SCL
Таймаут спасает скетч, но не шину: линия всё ещё прижата. Классический приём из апноутов NXP — тактовая реанимация. Логика такая: слейв, застрявший на середине байта, ждёт тактов на SCL, чтобы дожать свои биты; дадим ему их — и он отпустит SDA. Алгоритм: отпустить шину (Wire.end()), перевести A5 в выход и дёргать SCL до девяти раз импульсами по 5–10 микросекунд, наблюдая за SDA; как только SDA отпустилась вверх — прижать её на мгновение при высоком SCL и отпустить: это стоп-условие, после которого слейв обязан вернуться в покой. Затем Wire.begin() заново — и шина жива. Девять тактов — не магия: слейв не может ждать больше девяти битов, столько максимум занимает транзакция.
Мой порядок действий на зависшей плате такой: сканер, чтобы понять, кто отвечает; таймаут, чтобы скетч больше не вяз; реанимация, если линии прижаты. И последнее — реанимацию я ставлю не руками, а автоматически: три пропущенных пинга подряд — и скетч сам прогоняет девять тактов. За два года после этого мне ни разу не понадобилось ехать на балкон с табуреткой.
- Wire.requestFrom() и endTransmission() блокируют: застрявший слейв превращает их в вечное ожидание
- Wire.setWireTimeout(25000, true) прерывает зависшую транзакцию и сбрасывает шину; флаг getWireTimeoutFlag() докладывает о срабатывании
- Слейв, застигнутый сбросом мастера посреди байта, держит SDA; лечится до девяти тактами SCL и стоп-условием
- Сброс мастера не чинит шину: слейв не знает, что напарник перезагрузился
- У любого самодельного ожидания — дедлайн на millis(); для I2C 100 мс это щедрость
- 1. Что делает Wire.setWireTimeout(25000, true)?
- 2. Скетч начал висеть сразу после перепрошивки. Что вероятнее всего случилось?
- 3. Сколько тактов на SCL рекомендуют для реанимации шины?
- 4. Чему равен лимит на удержание линии слейвом в стандарте SMBus?
- 5. while (!Wire.available()) без requestFrom() перед ним — это:
Главное из урока: зависание — не мистика, а конкретная линия, которую держит конкретное устройство, и конкретный цикл без дедлайна. Диагност из симулятора — это половина реального скетча: на плате к нему добавляются setWireTimeout() и девять тактов реанимации. В следующем уроке уедем дальше — буквально: посмотрим, сколько выдерживает шина на длинных проводах и чем помогают буферы-повторители, когда датчик стоит в теплице, а мастер — у окна.
Частые вопросы по теме урока
- Что делает Wire.setWireTimeout(25000, true)?
- Первый аргумент — предел в микросекундах, второй приказывает библиотеке перезапустить TWI-железо после срабатывания. Прерывает транзакцию, застрявшую дольше 25 мс, и сбрасывает контроллер шины
- Скетч начал висеть сразу после перепрошивки. Что вероятнее всего случилось?
- Мастер сбросился на середине байта, слеву не хватило тактов — линия осталась прижатой. Лечится тактовой реанимацией. Слейв застрял посреди транзакции и держит SDA
- Сколько тактов на SCL рекомендуют для реанимации шины?
- Слейв не может ждать больше девяти битов — максимальной длины транзакции. Апноуты NXP говорят «до девяти». До девяти
- Чему равен лимит на удержание линии слейвом в стандарте SMBus?
- 35 мс. Если линия прижата дольше — это уже не штатная пауза, а застрявший слейв, и его надо реанимировать. 35 миллисекунд
- while (!Wire.available()) без requestFrom() перед ним — это:
- Буфер приёма наполняет requestFrom(). Без него available() не вырастет никогда — мой рекорд такого цикла трое суток. Вечный цикл: буфер пуст и не пополнится