Реле света в котельной я повесил на ESP32 с WiFi, и три месяца всё было хорошо. Потом начались чудеса: команда с панели доезжала через секунду, то не доезжала вовсе, то приезжала дважды. Разбирался вечерами: соседи поставили вторую точку доступа на моём канале, микроволновка на кухне глушила половину пакетов, а роутер по расписанию перезагружался и уносил MQTT-сессию с собой. Свет — та нагрузка, где каждый из этих глюков виден рукой: щёлк — а света нет.
Лечится железом, а не кодом: W5500 — чип с аппаратным TCP/IP-стеком и разъёмом RJ45, восемь сокетов, свой буфер на тридцать два килобайта. Для Arduino он виден библиотекой Ethernet.h: объект EthernetClient скармливается PubSubClient так же, как WiFiClient, — библиотеке MQTT всё равно, что у неё под низом, конструктор принимает любой Client. Первый скетч PubSubClient я разбирал в уроке про первый клиент — там под ним WiFi, здесь будет провод.
План такой: сперва цифры — почему провод честнее эфира для реле и света; потом полный скетч с DHCP и статическим фоллбэком; затем три грабли шилда, на которых я потерял вечер, — CS-пин 10, SD-карта на общей SPI-шине и сброс чипа после подачи питания. И честная таблица потребления: W5500 на активном линке берёт порядка 132 мА — узел для розетки, а не для батарейки.
Провод против эфира: цифры
Начну с задержки, потому что она ощущается руками. Проводной линк на W5500 даёт круговой пинг до брокера в одну-две миллисекунды; WiFi на ESP32 в той же комнате — от десяти до сорока, в зависимости от загрузки канала и числа соседей, делящих с вами эфир. Для телеметрии разница неважна, для света и реле — заметна: команда «включить» с панели доезжает мгновенно, без узнаваемого «подумал и включился».
Вторая цифра — устойчивость канала 2,4 ГГц. Микроволновая печь греет на частоте 2450 МГц — ровно посередине WiFi-диапазона, и пока она работает, пакеты едут с помехами и повторами. Плюс соседские точки, Bluetooth, радионяни. Витая пара от всего этого не зависит: свитч в щитке, экранированный кабель, и ни одна микроволновка в мире на него не влияет. Узел, который обязан слушаться всегда, я теперь сажаю на провод по умолчанию.
Ток — третье число. W5500 на активном линке 100 Мбит берёт около 132 мА, в режиме power-down опускается до 13; у ESP32 WiFi при передаче пики доходят до 240 мА при среднем 80-120. Оба числа делают проводной клиент узлом для розетки: 132 мА съедают 18650 меньше чем за сутки. Зато в момент передачи провод ничего не просаживает — проблемы brownout на пиках WiFi разобраны в уроке про питание ESP32, у провода их просто нет.
#include <SPI.h>#include <Ethernet.h>#include <PubSubClient.h> byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x2A}; // любой уникальный в сетиIPAddress broker(192, 168, 1, 50);const int PIN_RELAY = 7; EthernetClient net;PubSubClient client(net); void callback(char* topic, byte* payload, unsigned int len) { if (payload[0] == '1') { digitalWrite(PIN_RELAY, HIGH); } if (payload[0] == '0') { digitalWrite(PIN_RELAY, LOW); }} void setup() { Serial.begin(9600); pinMode(PIN_RELAY, OUTPUT); pinMode(10, OUTPUT); // CS W5500: нога 10 обязана быть OUTPUT pinMode(4, OUTPUT); digitalWrite(4, HIGH); // SD-карта шилда: отцепили от SPI-шины delay(50); // W5500 выходит из reset после подачи питания if (Ethernet.begin(mac) == 0) { // DHCP не ответил IPAddress ip(192, 168, 1, 121); // фоллбэк - статика Ethernet.begin(mac, ip); } Serial.print("IP: "); Serial.println(Ethernet.localIP()); client.setServer(broker, 1883); client.setCallback(callback); client.connect("kotelnaya-relay"); // ClientID уникальный client.subscribe("kotelnaya/relay/set");} void loop() { if (Ethernet.linkStatus() == LinkOFF) { // кабель выдернут Serial.println("[lin] LinkOFF: жду кабель"); delay(2000); return; } if (!client.connected()) { client.connect("kotelnaya-relay"); delay(2500); if (client.connected()) { client.subscribe("kotelnaya/relay/set"); } } client.loop();}Разбор по строкам. MAC-адрес у Ethernet-шилда не прошит заводом — придумываем сами, и единственное правило: уникальный внутри сети. Две платы с одним MAC живут до первого ARP-конфликта, а ловится он больно: обе платы то работают, то разом молчат. Я веду таблицу MAC в журнале узлов — занудство на две минуты при запуске, экономит час диагностики потом.
Ethernet.begin(mac) — это DHCP: плата просит адрес у роутера, на это уходит одна-две секунды. Вернулся ноль — сервер адрес не выдал, и здесь мой выбор: не умирать, а встать на запасной статический IP. Плюс DHCP — адреса раздаёт роутер, минус — адрес может смениться после его перезагрузки; плюс статика — адрес вечен, минус — следите, чтобы он не попал в чужой арендный диапазон. Локальный адрес после подключения печатает Ethernet.localIP() — строка живёт в каждой моей прошивке: IP узла без неё не вспомнишь.
LinkOFF в loop проверяется раньше MQTT: обрыв кабеля виден на уровне чипа сразу, без ожидания keepalive — полутора периодов pingreq, которые считались в уроке про keepalive. Дальше — обычная машина состояний связи: connect, переподписка, client.loop(); полную схему с backoff я собирал в уроке про переподключение, для провода она та же — обрывы реже, зато диагностика мгновенная. Бонус кучи: TCP/IP-стек живёт в кремнии W5500, и в двух килобайтах SRAM Uno под него не съедается ничего — крупным едоком остаётся буфер PubSubClient, чьи аппетиты считались в уроке про буфер 512.
Грабли шилда: CS, SD и reset
Грабля первая — CS-пин 10. W5500 слушает шину SPI, когда его нога выборки прижата к земле, а на Uno аппаратная выборка — нога 10, и библиотека требует pinMode(10, OUTPUT) при старте, даже если вы перенесли CS на другую ногу. Пропустили строку — контроллер SPI Uno выходит из режима мастера, шина не заводится, begin() отвечает чепухой. Мой случай: переезд с W5100 на W5500, скетч тот же, сети нет — четыре вечера ушли на одну строку pinMode.
Вторая — SD-карта. Классический Ethernet-шилд несёт слот microSD, и карта сидит на той же SPI-шине со своей выборкой на ноге 4. Неинициализированная карта держит линию MISO и глушит трафик: W5500 отвечает, а вы читаете шум. Лечение — pinMode(4, OUTPUT) и digitalWrite(4, HIGH): выборка поднята, карта отцеплена от шины. Когда карта нужна — инициализирую SD.begin(4) до Ethernet.begin и на двух шилдах порядок ни разу не подвёл.
CS можно и переносить: Ethernet.init(пин) говорит библиотеке, что выборка теперь на другой ноге — удобно, когда нога 10 уже занята щитом сверху. Единственное, что остаётся неизменным: pinMode на выбранной ноге обязан быть OUTPUT до begin(), и после init() порядок вызовов не меняется. У меня один перенос дожил до продакшна — на самодельной плате, где ногу 10 съело реле; с тех пор правило «сначала карта пинов, потом заказ шилда» написано у меня на первой странице журнала.
Третья — сброс. По даташиту ногу RSTn после подачи питания надо держать прижатой минимум пятьсот микросекунд. На модулях с RC-цепочкой это происходит само; на голом чипе — обязанность скетча: delay(50) до Ethernet.begin покрывает и reset, и стабилизацию питания. Без паузы begin() может ответить успехом, а первый же сокет откроется с мусором в регистрах — класс «иногда работает», самый дорогой класс неисправностей.
| Критерий | W5500, провод | ESP32, WiFi |
|---|---|---|
| Круговая задержка до брокера | 1-2 мс на домашнем свитче | 10-40 мс, зависит от канала 2,4 ГГц |
| Ток активного узла | около 132 мА, power-down 13 мА | среднее 80-120, пики до 240 мА |
| Помехи и соседи | витая пара, микроволновка не мешает | 2,4 ГГц: микроволновки, Bluetooth, точки соседей |
| Где уместен | реле, свет, стационар у розетки | переносные узлы и датчики на батарейках |
Из таблицы следует простая развилка. Стационарный узел, у которого есть розетка и куда можно дотянуть патч-корд, — берите провод: латентность ниже, помех нет, диагностика честнее, и ночная перезагрузка роутера не уносит с собой MQTT-сессию. Переносной датчик или узел на батарейке — провод противопоказан: 132 мА против десятков микроампер дремлющего ESP8266, разница в тысячи раз. Батарейный цикл с публикацией раз в час считался в уроке про датчик на батарейке.
Отдельное слово про реле, потому что это мой главный аргумент в спорах. Реле — узел-исполнитель: провал доставки виден сразу, рука тянется к выключателю, которого нет. Проводной клиент вычищает класс отказов «эфир занят» целиком, и единственной точкой отказа остаётся брокер. Для узлов, которые обязаны слушаться всегда, провод дешевле любых хитростей с QoS и повторами.
Про сокеты, потому что это вторая цифра после тока: W5500 держит восемь сокетов одновременно, и под них отведён общий буфер тридцать два килобайта — по умолчанию по два килобайта на приём и передачу каждому сокету. MQTT-клиент ест ровно один сокет, а PubSubClient сверху накладывает свой лимит на длину пакета; итоговая граница полезной нагрузки — минимум из этих двух чисел. Для звонка и реле запас бездонный, для пакетов в килобайт — уже считайте, как я считал буфер в уроке про буфер PubSubClient.
И последнее: модуль против шилда. Полноформатный шилд на Uno — воткнул и работает, но съедает свою высоту и все ноги SPI. Модуль-«кирпичик» с тем же чипом живёт на макетке и подключается пятью проводами — питание 3,3 В у большинства плат уже встроено в модуль, но проверьте: подадите 5 В на голый чип без стабилизатора — посчитаете модуль расходником. Разницу между «выбрал плату под задачу» и «выбрал плату, потому что она была в ящике» я формулирую каждый раз, когда в четвёртый раз перетыкаю шилд между Uno и Mega.
- 1. Что передать в конструктор PubSubClient для узла на W5500?
- 2. Ethernet.begin(mac) вернул ноль. Что произошло?
- 3. Зачем digitalWrite(4, HIGH) на шилде со слотом SD?
- 4. Сколько по даташиту держать RSTn прижатым к земле после подачи питания?
- 5. Узел на W5500 и узел на ESP32 WiFi в одной комнате. Чья задержка до брокера ниже?
- EthernetClient скармливается PubSubClient как WiFiClient: библиотеке MQTT всё равно, что под ней.
- DHCP через Ethernet.begin(mac), при отказе — статический фоллбэк; адрес печатает Ethernet.localIP().
- Грабли шилда: pinMode(10, OUTPUT) всегда, SD на ноге 4 отцепить digitalWrite(4, HIGH), пауза 50 мс после питания.
- Провод: RTT 1-2 мс и ноль проблем с 2,4 ГГц; W5500 берёт около 132 мА — узел для розетки, не для батарейки.
- Ethernet.linkStatus() == LinkOFF видит обрыв кабеля сразу, без ожидания keepalive брокера.
Частые вопросы по теме урока
- Что передать в конструктор PubSubClient для узла на W5500?
- Библиотека принимает любой Client: под WiFi — WiFiClient, под Ethernet — EthernetClient, сигнатура одна. EthernetClient net;
- Ethernet.begin(mac) вернул ноль. Что произошло?
- begin с одним аргументом mac — это запрос DHCP: ноль значит, сервер адрес не выдал. Дальше — статический фоллбэк. DHCP-сервер не выдал адрес
- Зачем digitalWrite(4, HIGH) на шилде со слотом SD?
- Карта сидит на той же SPI-шине с выборкой на ноге 4: неинициализированная, она глушит MISO и трафик W5500. Отцепить SD-карту от общей SPI-шины
- Сколько по даташиту держать RSTn прижатым к земле после подачи питания?
- Минимум 500 мкс; в скетче я закрываю вопрос delay(50) до Ethernet.begin — с запасом и на стабилизацию питания. Пятьсот микросекунд
- Узел на W5500 и узел на ESP32 WiFi в одной комнате. Чья задержка до брокера ниже?
- Проводной линк даёт единицы миллисекунд; WiFi тратит время на contention канала и повтори при помехах. У W5500: 1-2 мс против 10-40 мс у WiFi