Добрый день, уважаемый читатель!
У ESP32, как известно, имеется встроенный сетевой интерфейс для подключения к сети WiFi. И в подавляющем большинстве случаев все им и пользуются. Но WiFi не всегда и не везде применим, иногда все решает только подключение через кабель – Ethernet. Например, если ваше устройство будет работать в закрытом металлическом ящике или на значительном удалении от какой-либо точки доступа. ESP32 отнюдь не ограничен возможностями WiFi и BT, на нем вполне можно использовать и Ethernet, и GPRS/LTE (PPP over UART).
Некоторое время назад, я публиковал обзор платы Kincony KC868-A16, на которой предусмотрен ethernet-интерфейс. Вот на её примере я и попробую рассказать, как подключить ESP32 к сети не по воздуху, а кабелем.
Немного теории
Когда достаточно давно я написал статью, как подключать ESP32 с использованием ESP-IDF к сети WiFi, там было приведено описание шагов, которые необходимо предпринять для создания сетевого подключения. В частности – необходимость написания обработчиков событий WiFi и инициализация промежуточного TCP/IP интерфейса ESP-NETIF. При этом читатели иногда оставляли под статьей примерно такие комментарии: “а зачем мне знать про все эти ESP-NETIF и низкоуровневые процедуры“? Действительно, а зачем? Зачем espressif так заморочился и наворотил сложностей? Неужели нельзя было сделать по простому, по ардуински – “wifi_connect()”, а все остальное спрятать от излишне любопытных пользователей внутри платформы ESP-IDF?
Дело в том, что адаптер ESP-NETIF (NETwork InterFace) служит посредником между драйвером ввода-вывода (WiFi, ETHERNET, GPRS…) и сетевым стеком, обеспечивая маршрутизацию пакетов данных между ними. Этот абстрактный слой позволяет микроконтроллеру автоматически переключаться между WiFi или Ethernet соединениями автоматически, без использования дополнительного кода, но с использованием заранее заданных программистом приоритетов. Кроме того, ESP-NETIF полностью изолирует ваш прикладной код от сетевого стека ESP-IDF – для всего, что вам может потребоваться, вы должны использовать либо NETIF, либо более высокоуровневые функции. ESP-IDF в настоящее время реализует ESP-NETIF только для библиотеки lwIP TCP/IP. Однако сам адаптер не зависит от конкретной реализации TCP/IP и допускает различные варианты. Подробнее об NETIF читайте в официальном руководстве, (как всегда немного запутанном). Ну и на этом, про NETIF в рамках данной статьи, пожалуй достаточно.
Последовательность действий для подключения к сети через кабель почти ничем не отличается от подключения к сети WiFi, даже немного проще – ведь при проводном подключении не нам не потребуется авторизация. Те же самые обработчики сетевых событий, примерно та же самая их последовательность:
- Установить сетевой драйвер MAC и PHY, соответствующий вашем чипу.
- Создать новый экземпляр ESP-NETIF для ethernet-подключения. При этом мы можем задать приоритет подключения, с помощью которого система будет определять, какой сетевой интерфейс следует использовать в первую очередь, по возможности.
- И, как всегда в китайских поделках, приклеить созданный экземпляр ESP-NETIF к сетевому драйверу. Да, да, с помощью клея, правда виртуального.
- Создать и зарегистрировать обработчики событий ETHERNET_EVENT_START, ETHERNET_EVENT_STOP, ETHERNET_EVENT_CONNECTED, ETHERNET_EVENT_DISCONNECTED, IP_EVENT_ETH_GOT_IP
- Нажать красную кнопку с надписью esp_eth_start.
Об этом и пойдет речь в данной статье.
Принципы передачи данных в сети Ethernet
В начале нам необходимо прояснить, каким именно физическим способом мы будем подключать микроконтроллер к сети. Как и в случае с другими интерфейсами, нам потребуется некое устройство – приемопередатчик, которое сможет отправлять и получать пакеты данных, преобразовывая их в электрические сигналы. И если приемопередатчик WiFi уже встроен в чип ESP32, то в случае с ethernet нам придется задействовать внешнее устройство.
Если говорить по простому, то драйвер Ethernet-подключения состоит из двух основных частей – MAC и PHY:
- MAC – реализует канальный уровень передачи данных (аббревиатура от англ. Media Access Control), это цифровой интерфейс, который отвечает за управление и подключение физической среды физического уровня.
- PHY – реализует физический уровень, то есть это уже приемопередатчик электрических сигналов (аббревиатура от англ. Physical layer), который “занимается” формированием и получением аналоговых сигналов, которые “бегут” по витой паре сетевого кабеля к роутеру и обратно. Интерфейс PHY состоит из двух независимых каналов для передатчика и приемника.
При этом интерфейс MAC может быть реализован в самой ESP32, а вот для PHY уровня нам по любому потребуется дополнительная микросхемка. ESP-IDF на текущий момент поддерживает несколько ethernet-чипов, в том числе “внутренние” EMAC и “внешние” с SPI-интерфейсом:
- Davicom DM9051 SPI to Ethernet MAC Controller
- Texas Instuments DP83848 Single Port 10/100 Mb/s Ethernet Physical Layer Transceiver
- IC+ IP101 Single Port 10/100 MII/RMII/TP/Fiber Fast Ethernet Transceiver
- Micrel KSZ80xx 10Base-T/100Base-TX/100Base-FX Physical Layer Transceiver
- Microchip KSZ8851SNL Single-Port Ethernet Controller with SPI
- SMSC LAN87xx Small Footprint RMII 10/100 Ethernet Transceiver
- Realtek RTL8201 10/100M Fast Ethernet Phyceiver
- WIZnet W5500 Hardwired TCP/IP embedded Ethernet controller
В микросхемах DM9051, KSZ8851 и W5500 реализован как PHY, так и MAC-контроллер, поэтому их можно связать с ESP32 с помощью SPI-интерфейса. В данной статье я не буду их рассматривать, просто потому, что с ними не работал.
Для остальных чипов для связи уровней MAC и PHY придется воспользоваться интерфейсом MII (аббревиатура от англ. Media Independent Interface). Каждый уровень имеет свои данные, тактовые и управляющие сигналы. Интерфейс управления — это двухсигнальный интерфейс: один — тактовый сигнал, а другой — сигнал данных. Через интерфейс управления верхние уровни могут полностью отслеживать и контролировать PHY. Всего для интерфейса данных MII требуется аж 18 сигналов. Для микроконтроллеров это слишком жирно – почти все GPIO уйдут на обслуживание Ethernet. Поэтому в ESP32 для связи с общественностью PHY уровнем может быть использована облегченная версия интерфейса RMII (аббревиатура от англ. Reduced Media Independent Interface), которая требует всего 9 сигналов (GPIO). Но при этом PHY и MAC синхронизируются уже только одним и тем же источником таковых сигналов REF_CLK 50MHz, который должен обладать достаточно высокой стабильностью.
ESP-IDF Ethernet API предусматривает три варианта генерации опорного сигнала:
- a) C помощью внутреннего генератора ESP 25 Mhz с умножителем
- b) C помощью внешнего кварцевого генератора 50 MHz
- c) Некоторые контроллеры EMAC могут генерировать REF_CLC помощью внутренней высокоточной ФАПЧ.
В случае использования внутреннего тактового сигнала следует выбрать режим CONFIG_ETH_RMII_CLK_OUTPUT, в случае использования внешнего тактового сигнала – режим CONFIG_ETH_RMII_CLK_INPUT. При этом следует помнить, что для вывода тактового сигнала можно использовать только GPIO0 / GPIO16 / GPIO17, а для его получения извне – только GPIO0. Да и GPIO16 / GPIO17 могут быть заняты внешним SPIRAM модулем памяти. Поэтому для использования в качестве опорного сигнала в некоторых случаях остается только GPIO0. Но GPIO0 – довольно важный strapping pin, который отвечает за перевод контроллера в режим загрузки прошивки. Поэтому, если вы собираетесь использовать внешний опорный генератор, то на время сброса ESP тактовый сигнал необходимо отключить, программным или “аппаратным” способом, например отключив питание кварцевого генератора.
На Kincony KC868-A16 использован чип линейки LAN87xx с внутренним тактовым генератором, и для передачи тактового сигнала использован GPIO17, так как SPIRAM не предусмотрено.
В итоге параметры Ethernet интерфейса на Kincony KC868-A16 выглядят так:
// EN: Ethernet (LAN8720) // RU: Ethernet (LAN8720) #define CONFIG_ETH_PHY_ADDR -1 // PHY address according your board schematic, range: -1..31. Set to -1 to driver find the PHY address automatically. #define CONFIG_ETH_PHY_TYPE ETH_PHY_LAN87XX #define CONFIG_ETH_CLK_MODE EMAC_CLK_OUT #define CONFIG_ETH_GPIO_POWER_PIN -1 #define CONFIG_ETH_GPIO_CLK 17 #define CONFIG_ETH_GPIO_MDIO 18 #define CONFIG_ETH_GPIO_TXD0 19 #define CONFIG_ETH_GPIO_TX_EN 21 #define CONFIG_ETH_GPIO_TXD1 22 #define CONFIG_ETH_GPIO_MDC 23 #define CONFIG_ETH_GPIO_RXD0 25 #define CONFIG_ETH_GPIO_RXD1 26 #define CONFIG_ETH_GPIO_RX_DI 27
На этом, полагаю, с физическими основами, в данном конкретном случае уже можно и закончить. Давайте прекратим это словоблудие и начнем уже давить кнопочки компуктера.
Программирование
Перво-наперво необходимо подключить к проекту необходимые API:
#include "esp_netif.h" #include "esp_eth.h" #include "esp_event.h"
Объявляем глобальные (в рамках модуля) переменные для хранения указателей на различные драйверы и интерфейсы:
static esp_eth_mac_t *_mac = nullptr; static esp_eth_phy_t *_phy = nullptr; static esp_netif_t *_eth_netif = nullptr; static esp_eth_netif_glue_handle_t _eth_netif_glue = nullptr; static esp_eth_handle_t _eth_handle = nullptr;
Для чего они нужны – будет понятно ниже.
Устанавливаем драйвер
Теперь можно установить драйвер “встроенного” MAC. Делается это не просто, а очень просто:
// Init common MAC configs to default eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); // Init vendor specific MAC config to default eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); // Update vendor specific MAC config based on board configuration esp32_emac_config.interface = EMAC_DATA_INTERFACE_RMII; esp32_emac_config.smi_mdc_gpio_num = CONFIG_ETH_GPIO_MDC; esp32_emac_config.smi_mdio_gpio_num = CONFIG_ETH_GPIO_MDIO; esp32_emac_config.clock_config.rmii.clock_mode = CONFIG_ETH_CLK_MODE; esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t)CONFIG_ETH_GPIO_CLK; // Create new ESP32 Ethernet MAC instance _mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
После этого можно установить драйвер и для железяки:
// Init common PHY configs to default eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); phy_config.phy_addr = CONFIG_ETH_PHY_ADDR; phy_config.reset_gpio_num = CONFIG_ETH_GPIO_POWER_PIN; // Create new PHY instance based on board configuration #if CONFIG_ETH_PHY_TYPE == ETH_PHY_IP101 ESP_LOGD(logTAG, "Selected PHY: IP101"); _phy = esp_eth_phy_new_ip101(&phy_config); #elif CONFIG_ETH_PHY_TYPE == ETH_PHY_RTL8201 ESP_LOGD(logTAG, "Selected PHY: RTL8201"); _phy = esp_eth_phy_new_rtl8201(&phy_config); #elif CONFIG_ETH_PHY_TYPE == ETH_PHY_LAN87XX ESP_LOGD(logTAG, "Selected PHY: LAN87XX"); _phy = esp_eth_phy_new_lan87xx(&phy_config); #elif CONFIG_ETH_PHY_TYPE == ETH_PHY_DP83848 ESP_LOGD(logTAG, "Selected PHY: DP83848"); _phy = esp_eth_phy_new_dp83848(&phy_config); #elif CONFIG_ETH_PHY_TYPE == ETH_PHY_KSZ80XX ESP_LOGD(logTAG, "Selected PHY: KSZ80XX"); _phy = esp_eth_phy_new_ksz80xx(&phy_config); #endif
Здесь я “выбираю” нужный драйвер с помощью условных макросов препроцессора, но это не обязательно, можно выбрать один и сразу.
Когда все готово, можно уже установить сам драйвер:
// Init Ethernet driver to default and install it esp_eth_config_t config = ETH_DEFAULT_CONFIG(_mac, _phy); esp_err_t ret = esp_eth_driver_install(&config, &_eth_handle); if (ret == ESP_OK) { ESP_LOGI(logTAG, "Ethernet driver installed"); } else { ESP_LOGE(logTAG, "Failed to install ethernet driver: %d (%s)", ret, esp_err_to_name(ret)); if (_eth_handle != nullptr) { esp_eth_driver_uninstall(_eth_handle); }; if (_mac != nullptr) _mac->del(_mac); if (_phy != nullptr) _phy->del(_phy); }
Код выглядит громоздко, но на самом деле работает только одна строчка. Итак, поздравляю вас, самый первый этап мы выполнили. Идем дальше.
Создаем системный цикл событий и обработчики
Так как вся система связи, как и в случае с wifi, работает на событиях, нам нужно запустить цикл событий по умолчанию. Но он может быть запущен ранее, поэтому просто игнорируем ошибку ESP_ERR_INVALID_STATE:
// Start the system events task esp_err_t ret = esp_event_loop_create_default(); if (ret == ESP_ERR_INVALID_STATE) ret = ESP_OK; if (ret != ESP_OK) { ESP_LOGE(logTAG, "Failed to create default event loop: %d (%s)", ret, esp_err_to_name(ret)); return ret; };
Обработчиков у меня два. В принципе, ничего особо важного они не делают, просто выводят сообщения в лог и ретранслируют события дальше, в прикладной цикл событий – по ним уже запускаются другие задачи и процессы. Измените их по своему вкусу:
static void ethernetEventHandler_Ethernet(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { uint8_t mac_addr[6] = {0}; switch (event_id) { case ETHERNET_EVENT_CONNECTED: esp_eth_ioctl(_eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr); ESP_LOGI(logTAG, "Ethernet link up: mac address %02x:%02x:%02x:%02x:%02x:%02x", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); // Re-dispatch event to another loop eventLoopPost(RE_WIFI_EVENTS, RE_ETHERNET_CONNECTED, nullptr, 0, portMAX_DELAY); break; case ETHERNET_EVENT_DISCONNECTED: ESP_LOGI(logTAG, "Ethernet link down"); // Re-dispatch event to another loop eventLoopPost(RE_WIFI_EVENTS, RE_ETHERNET_DISCONNECTED, nullptr, 0, portMAX_DELAY); break; case ETHERNET_EVENT_START: ESP_LOGI(logTAG, "Ethernet started"); // Re-dispatch event to another loop eventLoopPost(RE_WIFI_EVENTS, RE_ETHERNET_STARTED, nullptr, 0, portMAX_DELAY); break; case ETHERNET_EVENT_STOP: ESP_LOGI(logTAG, "Ethernet stopped"); // Re-dispatch event to another loop eventLoopPost(RE_WIFI_EVENTS, RE_ETHERNET_STOPPED, nullptr, 0, portMAX_DELAY); break; default: break; }; } static void ethernetEventHandler_GotIp(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_data) { ip_event_got_ip_t * data = (ip_event_got_ip_t*)event_data; eventLoopPost(RE_WIFI_EVENTS, RE_ETHERNET_GOT_IP, data, sizeof(ip_event_got_ip_t), portMAX_DELAY); // Log #if CONFIG_RLOG_PROJECT_LEVEL >= RLOG_LEVEL_INFO uint8_t * ip = (uint8_t*)&(data->ip_info.ip.addr); uint8_t * mask = (uint8_t*)&(data->ip_info.netmask.addr); uint8_t * gw = (uint8_t*)&(data->ip_info.gw.addr); ESP_LOGI(logTAG, "Ethernet got IP-address: %d.%d.%d.%d, mask: %d.%d.%d.%d, gateway: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3], mask[0], mask[1], mask[2], mask[3], gw[0], gw[1], gw[2], gw[3]); #endif }; }
Инициализируем TCP/IP
Теперь все готово для того, чтобы создать новый экземпляр NETIF специально для Ethernet:
// Initialize TCP/IP network interface aka the esp-netif (should be called only once in application) ret = esp_netif_init(); if (ret == ESP_ERR_INVALID_SIZE) ret = ESP_OK; // Create instance of esp-netif for Ethernet esp_netif_inherent_config_t netif_config = ESP_NETIF_INHERENT_DEFAULT_ETH(); netif_config.if_key = "ETH"; netif_config.if_desc = "Ethernet"; netif_config.route_prio = 32767; esp_netif_config_t config = { .base = &netif_config, .driver = nullptr, .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH }; _eth_netif = esp_netif_new(&config); if (_eth_netif == nullptr) { ESP_LOGE(logTAG, "Failed to create netif interface"); return ESP_FAIL; };
Обратите внимание! Здесь я задал максимально возможный приоритет для Ethernet-интерфейса – 32767. Поэтому когда подключен сетевой кабель, все пакеты будут идти именно через этот интерфейс. Но стоит нам отключить кабель от платы, wifi автоматически вступает в работу (при условии, что он подключен, конечно). Можно так же подправить и библиотеку wifi, указав желаемый приоритет и там, но я пока не стал. Работает и так.
Клеим!
Позвольте! – скажут внимательные читатели – А как же связан NETIF и MAC? И будут совершенно правы! Ведь мы ранее установили драйвер, но при создании NETIF нигде его не использовали…
Берем баночку цианоакрилата или хотя бы термоклей, намазываем и крепко прижимаем друг к другу на несколько секунд. А если серьезно, то так:
_eth_netif_glue = esp_eth_new_netif_glue(_eth_handle); if (_eth_netif_glue == nullptr) { ESP_LOGE(logTAG, "Failed to create netif glue"); return ESP_FAIL; }; // Attach Ethernet driver to TCP/IP stack ret = esp_netif_attach(_eth_netif, _eth_netif_glue);
Ну и если все до сих прошло гладко, без сучка и задоринки, то можно подключать созданные ранее обработчики:
// Register user defined event handers RE_ERROR_CHECK_EVENT(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ðernetEventHandler_Ethernet, NULL)); RE_ERROR_CHECK_EVENT(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ðernetEventHandler_GotIp, NULL));
3, 2, 1, ПУСК!
Просто оставлю это здесь:
ret = esp_eth_start(_eth_handle); if (ret != ESP_OK) { ESP_LOGE(logTAG, "Failed to start ethernet driver: %d (%s)", ret, esp_err_to_name(ret)); };
Подключаем кабель, и наслаждаемся тем, как мигают индикаторы и весело, с песнями и плясками, бегут по проводам маленькие байтики….
Как вы убедились, ничего сложного в этом нет. Получилось даже быстрее и проще, чем с WiFi-подключениями.
Ссылки
- Готовую библиотеку можно посмотреть и даже скачать тута: github.com/kotyara12/reEthernet. Однако её следует использовать с некоей осмотрительностью, так как она хочет утащить за собой еще небольшую кучку других моих библиотек
и завалить вас с головой. Но, к счастью, от них можно избавится, например rLog заменить на стандартный esp_log, а reEsp32 и reEvents убрать вовсе, немного изменив код. - Справочное руководство по ethernet API здеся: docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/network/esp_eth.html. Со временем ссылка может устареть по не зависящим от меня причинам. Прошу понять и простить.
Засим разрешите откланяться, с вами был ваш покорный слуга Александр aka kotyara12. И вы, это, заходите если что….
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью:
Имел опыт и со встроенным Ethernet и с SPI-Ethernet, и в первом случае скорость была 100 мбит/с, а во втором – где-то около 12 мбит/с при 36 МГц тактирования SPI
Тестил при помощи SpeedTest и YouTube
I’m from Vietnam. Thank you for sharing your knowledge. Do you have any articles on W5500 IDF ESP32?