Метеостанция на балконе прожила неделю, потом прошла морось — и экран замолчал. Хуже того: скетч продолжал работать бодро, последние цифры замерли на месте, и я ещё час искал брак в датчике давления, пока не дошло, что шина умерла раньше него. Тогда же я поймал вторую привычку-тупик: просил Serial печатать «отправлено», хотя ничего не проверял — байты складывались в буфер, а поехали ли они куда-то, скетч не спрашивал. Печаталось «отправлено» исправно — и введённый в заблуждение журнал показывал здоровый стенд, которого уже не было. Лекарство нашлось внутри самой библиотеки и весит одну строку: возвращаемое число endTransmission(). Оно было под рукой все эти годы — я просто игнорировал его, как и большинство начинающих: компилятор не заставляет проверять, а примеры в сети рисуют скетчи без единого взгляда на возврат.
Механика такая: beginTransmission() и write() не едут на шину вообще — они лишь складывают байты в 32-байтовый буфер, про который я подробно писал в уроке про анатомию библиотеки Wire. Вся передача происходит в момент endTransmission(): вот когда выходят START, адрес, данные и подтверждения. И она же возвращает код — самый быстрый тест здоровья каждого устройства на шине. В этом уроке расшифруем коды 0-4, соберём из них автодиагностику проекта и честно проговорим границу: в симуляторе шина исправна и всегда ACK, все пинги вернут ноль; на живой плате вернутся и 1, и 2, и 3, и 4.
Что значит каждый код
Ноль — успех: адрес и все байты подтверждены. Единица — данные не влезли в буфер: потолок 32 байта, больше за раз шина не увезёт. Двойка — NACK на адрес: мастер постучал, и никто не сказал «я здесь». Тройка — NACK на данные: устройство представилось, но принять байты отказалось. Четвёрка — всё прочее: шину прижало к земле, железо TWI запнулось, потерялся арбитраж. Подтверждение — это девятый бит каждого байта, приёмник прижимает SDA, показывая «принято»; вся механика бита ACK разбиралась в уроке про устройство шины. На ESP32-ядрах встречается ещё пятый код — таймаут, когда шина не отвечает вовсе. Логика за кодами простая: мастер и слейв договариваются на каждом байте, и вся диагностика спрятана в этом разговоре. Молчание на адрес — нет собеседника; молчание на данные — собеседник есть, но просит паузу; признание буфера — проблема в вашем скетче, а не у устройства. Отсюда и порядок чтения кода в чужом проекте: сначала смотрите, кто молчит — адресат или ваши байты, и только потом лезете в осциллограф.
Пустой пакет легален: beginTransmission и endTransmission без единого write — и на шину уходит только START, адрес и бит подтверждения. Это «стук в дверь», на нём построен сканер из урока про поиск адресов и «тонометр шины» из урока про номиналы подтяжек — там мы пинговали шину, чтобы поймать слабую подтяжку. Здесь же пинг превращается в штатную диагностику: список устройств проекта прошёлся при включении, каждый адрес получил вердикт. Дешевизна — главный аргумент: стук стоит 18 бит шины, около 180 мкс на сотне килогерц; даже дюжина устройств опрашивается за пару миллисекунд — дешевле, чем один кадр экрана.
| Код | Что случилось | Что делать |
|---|---|---|
| 0 | адрес и данные подтверждены | работаем дальше |
| 1 | посылка не влезла в 32-байтовый буфер | резать посылку на части по 30 байтов |
| 2 | NACK на адрес: устройства нет | проверить адрес (7 бит, не 8), питание, провода |
| 3 | NACK на данные: устройство занято | пауза 5-10 мс и повтор, это штатное поведение EEPROM |
| 4 | прочее: шину прижало к земле | Wire.end() и Wire.begin(), смотреть подтяжки и замыкания |
| 5 (только ESP32) | таймаут: шина молчит совсем | проверять SCL: кто-то растянул такт или линия в обрыве |
Двойка — самый частый гость, и список подозреваемых у неё короткий. Первый — адрес: в даташитах полно восьмибитных записей, где 0xD0 на деле значит 0x68, вся арифметика разбиралась в уроке про таблицу адресов. Второй — питание модуля: у меня экран на шлейфе жил от контакта, который отходил от вибрации, и двойка появлялась волнами. Третий — провода и общий минус. Порядок моей проверки такой: сначала сканером подтверждаю, что адрес вообще виден; потом смотрю питание модуля; потом шевелю провода при живом сканере — отходит контакт, который мультиметр называет целым. А вот тройка интереснее: она значит, что чип жив и занят. Классика — EEPROM 24C02: после записи байта чип несколько миллисекунд «варит» ячейку, и всё это время он подтверждает адрес, но отказывается принимать данные. Это не сбой, а штатный язык: «подожди». Пауза 5 мс и повтор — и обмен идёт дальше, сам цикл записи EEPROM мы мерили в уроке про первые настройки энергонезависимой памяти. Контраст важный: при двойке молчит весь чип, при тройке — только приём данных. И ещё одно различие: двойка и тройка — слова устройства, а единица — слово вашего скетча: буфер один и на всех, и переполнение означает, что скетч попросил шину увезти больше, чем та берёт.
Четвёртый код на практике выглядит как шина, прижатая к земле: слепые данные, обрыв, замыкание. Лечится перезапуском контроллера шины — Wire.end() с паузой и Wire.begin(). И не путайте этот код с вердиктом requestFrom(): чтение возвращает число принятых байтов, и ноль там тоже значит «устройство молчит».
#include <Wire.h> // Пинг при включении по списку устройств + контроль часов в loop byte ustrojstva[] = {0x27, 0x68, 0x50}; // LCD, часы DS3231, EEPROM 24C02const byte SKOLKO = 3; String verdikt(int kod) { if (kod == 0) return "отвечает"; if (kod == 1) return "не влезла в буфер"; if (kod == 2) return "по адресу никого"; if (kod == 3) return "занят, повторить позже"; return "шина молчит";} void setup() { Wire.begin(); Serial.begin(9600); Serial.println("Проверка стенда при включении"); for (byte i = 0; i < SKOLKO; i++) { Wire.beginTransmission(ustrojstva[i]); int kod = Wire.endTransmission(); // пустой пакет: только адрес и ACK Serial.print("adr="); Serial.print(ustrojstva[i], HEX); Serial.print(" kod="); Serial.print(kod); Serial.print(" -> "); Serial.println(verdikt(kod)); }} byte sboev = 0; // неудач подряд void loop() { Wire.beginTransmission(0x68); // здоровье часов — каждый цикл int kod = Wire.endTransmission(); if (kod == 0) { sboev = 0; } else { sboev++; } Serial.print("kod="); Serial.print(kod); Serial.print(" sboev="); Serial.println(sboev); if (sboev >= 3) { // три подряд — шину под лечение Wire.end(); // отпускаем шину delay(50); Wire.begin(); // поднимаем заново sboev = 0; Serial.println("шина перезапущена"); } delay(1000);}Проверка в симуляторе
Прямо скажу: в симуляторе шина исправна и всегда ACK — все три пинга вернут kod=0, счётчик сбоев будет вечно нулевым, а ветка перезапуска шины не сработает ни разу. Это свойство движка, а не упрощение ради красоты: он не эмулирует обрывы, отключённое питание и занятые записью чипы. Зато сам протокол подлинный: в панели I2C пустой пакет виден как START на адрес и сразу STOP — строка «Данные: ; STOP» и есть наш стук в дверь. На живой плате тот же скетч оживает: выньте питание часов — после трёх kod=2 подряд скетч сам перезапустит шину и напечатает об этом.
Про счётчик отдельно: я не реагирую на одиночный сбой — дребезг контакта может дать одну двойку и исчезнуть. Три подряд — уже статистика, после неё перезапуск шины оправдан; порог подобран на метеостанции, где морось укладывала шину на секунду, а не навсегда. В журнале я печатаю рядом с kod и время: пара kod и millis в одной строке даёт момент, когда шина поплохела, без секундомера и без догадок. Двойка с меткой времени ценнее любых предположений: она превращает «периодически отваливается» в «отваливается после дождя в 14:07».
Эксперименты для закрепления
Первое: расширьте список стенда адресами 0x3F (переходник LCD версии А) и 0x76 (BME280) — на живой плате отсутствующий адрес вернёт двойку, и вы получите список «кто дома» за один прогон. Второе: доведите посылку до 33 байтов — единица покажет переполнение буфера раньше, чем вы заметите потерю данных. Третье: напишите байт в EEPROM и запустите в цикле запись следующего без пауз — тройка будет выпадать, пока чип занят, и лечится это паузой 5 мс, а не перезапуском шины.
Четвёртое: в момент сбоя печатайте millis() — вы получите в журнале точную временную метку и по ней найдёте событие, уложившее шину; таймер без delay() собирается по классическому рецепту многозадачного loop. И пятое, для мультиплексорных схем из первого урока трио: пингуйте каждую подшину после открытия канала — так за один прогон вы проверите и чип на 0x70, и всех его жильцов.
- beginTransmission и write только наполняют 32-байтовый буфер; передача и вердикт — в endTransmission()
- Коды: 0 успех, 1 не влезло в буфер, 2 NACK на адрес, 3 NACK на данные, 4 прочее; на ESP32 бывает 5 — таймаут
- Пустой пакет без write легален: START, адрес, ACK — так работает сканер и быстрый пинг устройства
- Тройка на EEPROM после записи — штатная занятость: пауза 5 мс и повтор, чип «варит» ячейку
- Код 0 проверяет только связь; содержимое проверяет вторым эшелоном — чтением регистра-подписи
- 1. Что делают beginTransmission и write до вызова endTransmission?
- 2. Пустой пакет (без единого write) перед endTransmission — это:
- 3. EEPROM 24C02 через 5 мс после записи отказывается принимать данные (код 3). Это:
- 4. Внутри симулятора endTransmission всегда возвращает:
- 5. Код 2 (NACK на адрес) чаще всего означает:
Частые вопросы по теме урока
- Что делают beginTransmission и write до вызова endTransmission?
- На шину ничего не уходит: вся передача происходит внутри endTransmission(). складываются в 32-байтовый буфер
- Пустой пакет (без единого write) перед endTransmission — это:
- Так работает сканер адресов: на шину уходит START, адрес и ACK — быстрая проверка присутствия. легальный стук в дверь: только адрес и подтверждение
- EEPROM 24C02 через 5 мс после записи отказывается принимать данные (код 3). Это:
- Во время внутренней записи EEPROM подтверждает адрес, но NACK-ает данные. 5 мс — и обмен идёт. штатная занятость: пауза и повтор
- Внутри симулятора endTransmission всегда возвращает:
- Виртуальная шина исправна и всегда ACK; на живой плате вернутся и 1, и 2, и 3, и 4. 0
- Код 2 (NACK на адрес) чаще всего означает:
- На стук не ответили: проверяйте адрес (7 бит, не 8), питание модуля, SDA и общий минус. по адресу никого: неверный адрес, питание или провода