Особенности программирования ESP32 из Arduino IDE

Эта небольшая статья ориентирована в первую очередь на тех, кто только начинает осваивать микроконтроллер ESP32 в среде Arduino IDE. Я далеко не гуру в микроконтроллерах и в свое время я потратил много времени, пытаясь понять, чем же отличается программирование для ESP32 от уже привычных Adruino и ESP8266; и почему почти все примеры для ESP32 написаны как задачи для FreeRTOS. Меня долго занимал вопрос – что же это за FreeRTOS такая, и так ли уж необходимо ее осваивать, когда начинаешь работать с новым микроконтроллером. Забегая вперед скажу, что таки да, без FreeRTOS на ESP32 никуда, по крайней мере если Вы используете “классическую” Arduino IDE. Ну то есть если очень хочется, то можно обойтись без задач и FreeRTOS, но это будет как “микроскопом гвозди забивать”. Всё нижесказанное относится как к Arduino IDE, так и к PlatformIO (в случае использования framework-а ArduinoEspressif32, что практически равносильно Arduino IDE). Итак, обо всем по порядку… 

ESP32 имеет по сравнению с ESP8266 множество преимуществ – более шустрый двухядерный процессор, гораздо больше портов ввода-вывода, в том числе и аналоговых, возможность подключения внешней WiFi-антенны (на некоторых моделях) и много чего еще. В рамках данной статьи я не буду рассматривать все аппаратные отличия ESP32 от своего предшественника, информации об этом много. Скажу лишь, что лично для меня ключевым фактором перехода на повсеместное использование ESP32 стало существенно большее количество портов ввода-вывода для подключения внешних устройств и датчиков, особенно аналоговых. ESP8266 обладает в этом плане ну очень скромными возможностями, да еще единственный АЦП обладает довольно низким разрешением. Конечно, цена ESP32 несколько выше, но с этим вполне можно смириться. 

ESP32-DevKitC

ESP32-DevKitC – варианты с печатной антенной и внешней

Перед началом программирования ESP32 в Arduino IDE необходимо установить пакет esp32 by Espressiv Systems через Менеджер плат:

esp32 by Espressiv Systems

Установленный пакет esp32 в менеджере плат

Если в списке плат нет этого пакета, добавьте строчку “https://dl.espressif.com/dl/package_esp32_index.json” в Настройках Adruino IDE:

Настройки Adruino IDE

Подключение esp32 к менеджеру плат

После этого повторно установите пакет через Менеджер плат.

Нужна ли FreeRTOS для ESP32?

Первое, на что натыкаешься, когда смотришь примеры скетчей для ESP32 – в них очень часто используются функции xTaskCreate или xTaskCreatePinnedToCore. То есть различные операции выполняются в режиме многопоточности. Возникает вопрос: а разве нельзя сделать “по старому, по простому”, без использования всех этих задач? Так ли уж необходимо осваивать новую область – программирование для FreeRTOS?

Кроме того, лично у меня сразу же возник еще один вопрос – если на борту ESP32 теперь два процессора, то как они будут использоваться скетчем? В свое время я задавал на форме esp8266.ru, но потом сам же на него и ответил :). 

Ответы на свои вопросы я получил после того, как наткнулся на “обертку” скетча Arduino \packages\esp32\hardware\esp32\1.0.4\cores\esp32\main.cpp. Эта обертка используется для всех скетчей, которые Вы создаете в классической Arduino IDE.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_task_wdt.h"
#include "Arduino.h"

TaskHandle_t loopTaskHandle = NULL;

#if CONFIG_AUTOSTART_ARDUINO

bool loopTaskWDTEnabled;

void loopTask(void *pvParameters)
{
setup();
    for(;;) {
        if(loopTaskWDTEnabled){
            esp_task_wdt_reset();
        }
        loop();
    }
}

extern "C" void app_main()
{
    loopTaskWDTEnabled = false;
    initArduino();
    xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, &loopTaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
}

#endif

Какую информацию отсюда можно почерпнуть? Видно, что FreeRTOS всегда по умолчанию подключается к Вашему скетчу (через #include “freertos/FreeRTOS.h”), а любой классический скетч Arduino IDE (то есть написанный без использования задач) – это всего лишь задача FreeRTOS, которая выполняется на втором ядре процессора #1, и которой выделено всего 8192 байт стека! Сама FreeRTOS при этом выполняется на ядре процессора #0. Это и хорошо, и плохо. Хорошо, что на ESP32 под Arduino обслуживание самого контроллера, WiFi и прочие служебные надобности теперь никак не пересекаются с Вашим скетчем. Теперь даже если скетч зависнет или уйдет в Sleep на столетие, то это никак не повлияет на работу служебных подпрограмм. А плохо тем, что Ваш скетч получит только доступ только к ограниченному объему стека, остальная доступная память просто не будет использоваться. 8192 – это далеко не весь стек, что доступен задачам FreeRTOS, на одном из моих контроллеров работает еще с десяток параллельных задач (каждый со своим выделенным стеком) и память еще не закончилась.

Вывод: если Вы хотите использовать все доступные на ESP32 возможности, Вам придется изучать и использовать функции FreeRTOS, хотите Вы этого или нет. В этом нет ничего страшного, осваивается FreeRTOS довольно легко. Многопоточность позволяет легко и изящно отделить “мух от котлет”, когда требуется обеспечить стабильную работу автоматики вне зависимости от внешних факторов. Например у Вас “сгорел” WiFi-роутер, “упал” MQTT-сервер или случилось что-то ещё. Но при этом “основная” задача контроллера, где читаются показания датчиков и включаются и выключаются реле управления насосами и обогревателями продолжат стабильно и без задержек работать. На однопоточной системе сохранить такую стабильность гораздо сложнее. Но увы, когда Вы начнете использовать многопоточность и задачи на ESP, то Вам придется забыть про некоторые уже ставшие привычными библиотеки Arduino, так как они просто не способны работать в многозадачной среде. Например, для ESP32 не будет работать библиотека ESP8266Ping и Arduino Time Library. Проблема решаемая, но требуется внимательность и перепрограммирование некоторых участков кода под новые условия. 

А нужен ли вообще Arduino для ESP32?

Ещё более кардинальным решением является полный отказ от фреймворка ArduinoEspressif32 как такового, то есть программирование контроллера на более “низкоуровневом” фреймворке ESP-IDF. Зачем это нужно и какие возможности это дает?

Начнем с преимуществ ESP-IDF

Во-первых ArduinoEspressif32 – это всего-лишь “надстройка” над ESP-IDF, которая занимает лишние ресурсы и память. Фреймворк ArduinoEspressif32 создан тем же Espressif32 для интеграции ESP-IDF в среду Arduino IDE, но может быть использован и под PlatformIO (что в 99% случаев и делается). На мой лично взгляд это не самый оптимальный подход, так как код написанный под ESP-IDF будет по определению более оптимальным, чем код, выполняющий точно те же самые функции из под Arduino. Ну это если вы сами не налажаете конкретно, конечно 😉

Во-вторых, в ESP-IDF API разработчики уже предусмотрели столько встроенных функций, что необходимость в сторонних библиотеках просто отпадает. Встроенное API для подулючения к WIFI, поддержка протоколов SNTP, PING, MQTT, HTTP и HTTPS (методы GET/POST/DELETE и т.д.) и многие другие функции. Всё это достаточно просто запускается и управляется. В общем есть практически весь набор базовых функций для программирования микрокомпьютера. И никаких велосипедов изобретать не требуется. Просто поищи в документации.

В третьих, ArduinoEspressif32 – это однопоточная система по определению. Она не создавалась для многопоточных микроконтроллеров. И при одновременном обращении к одному и тому же ресурсу будут возникать конфликты, например при одновременном доступе к WiFi. Конечно, этим можно и нужно управлять. Но! На ESP-IDF некоторых конфликтов можно избежать, например при обращении из разных потоков к WiFi. Правда так следует поступать не всегда – при одновременном доступе к шине I2C или другому подобному ресурсу все-таки придется использовать механизмы блокировок.

В четвертых, ArduinoEspressif32 “теряет” некоторые аппаратные возможности ESP32. Например в ESP32 можно использовать две раздельные шины I2C (и навешать в два раза больше датчиков), но Arduino это не умеет.

Недостатки отказа от ArduinoEspressif32

В этом не совершенном мире за всё приходится платить. С чем придется столкнуться, если Вы решите использовать ESP32 “на полную катушку”?

Во-первых, и это очевидно, придется отказаться от “любимой и ненавистной” Arduino IDE и перейти на что-то другое. Например перейти на VSCode + PlatformIO. Скажу сразу – хоть VSCode и несравненно удобнее Arduino IDE, но плагин для работы с микроконтроллерами PlatformIO, лично на мой взгляд ещё далек от совершенства. Прежде чем можно будет собрать более-менее полноценный проект на PlatformIO, помучаешься изрядно. С чем это связано – читайте в другой статье.

Во-вторых, и это так же очевидно, таки придется осваивать новые функции встроенного API. Но это даже интересно. ESP-IDF по большей части асинхронное API, то есть основанное на событиях. Некоторые задачи решаются настолько “изящно”, что получаешь истинное удовольствие. А над решением других задач приходится поломать голову.

Во-третьих, на ESP32 каждая отдельная задача не может использовать весь доступный объем стека, ей выделяется только небольшой фиксированный кусочек. С этим приходится считаться, это несколько усложняет отладку, так как приходится каждый раз опытным путем подбирать объем стека для задачи. Дать больше – другим задачам может не хватить, дать меньше – контроллер будет перезагружаться из-за переполнения стека задачи.

Следующий недостаток, на мой взгляд, может напрочь перечеркнуть все достоинства отказа от Arduino что называется “на корню”. Дело в том, что под ESP-IDF практически нет библиотек драйверов устройств. Датчиков температуры и влажности, LCD, и много другого. Драйверов всего того, что легко находится в списке стандартных библиотек Arduino IDE. То есть все библиотеки, скажем для DHT22 или BME280, которые можно найти в репозиториях Arduino IDE и PlatformIO на момент написания статьи можно найти только под Arduino. Версий под ESP-IDF очень мало – DHT22, вроде бы есть ещё что-то, и на этом в общем-то всё. Их просто ещё никто не написал, ибо все плотно сидят на анаболиках Arduino. Это не катастрофа, библиотека для работы с I2C имеется, и “покурив” мануалы, можно написать свою либу. Но Вам придется её написать самому. Что я сейчас и делаю, в принципе. Это не сильно сложно, для большинства сенсоров (DHTxx, AHT10, SHT, DHT и т.д. я уже написал свои варианты, все они выложены в общем доступе.

Мои репозитории на GitHub: для ESP32 и не только

Использование Arduino Time Library

Весьма неприятной неожиданностью, с которой я столкнулся на ESP32, стала невозможность использования библиотеки Arduino Time Library (Time) из публичного каталога библиотек Arduino. Эту библиотеку я использовал для получения времени – даты и синхронизации времени с NTP-серверами. Сама по себе библиотека прекрасно компилируется под ESP32 и вроде бы работает. Но нормально работает в том случае, если Вы используете на ESP32 только одну единственную задачу. В противном случае может легко случиться ситуация, когда несколько задач попытаются одновременно получить время и, если при этом запустится синхронизацию с NTP, то это почти гарантированно приведет к зависанию контроллера.

Выход из данной ситуации я вижу в использовании “встроенных” в API функций даты – времени, например: 

// Запуск синхронизации с SNTP
if(sntp_enabled()){
sntp_stop();
}
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, sntpServer1);
sntp_setservername(1, sntpServer2);
sntp_setservername(2, sntpServer3);
sntp_setservername(3, sntpServer4);
sntp_setservername(4, sntpServer5);
sntp_init();

Потом синхронизацию выполняет уже ядро FreeRTOS самостоятельно. Оказалось, что наладить синхронизацию через встроенные в API функции даже проще, да и есть системная либа “time.h” где все реализовано удобнее и проще, и велосипеды изобретать не надо.

Ping на ESP32

Организовать проверку доступности серверов на ESP32 также, как и в случае с временем, достаточно просто организовать с помощью встроенных в API функций. Увы, но информации в сети об этом практически нет, и мне пришлось потратить много времени, чтобы запустить этот процесс. Вот пример:

#include "esp_ping.h"
#include "ping/ping.h"

static esp_err_t pingerOnReceive(ping_target_id_t msgType, esp_ping_found * pf)
{
  #if defined(rSerialDebug)
  Serial.printf("PING :: Ping statistics: sent = %d, recieved = %d, lost = %d, resp time = %d ms, avg = %.1f ms, total time = %d ms\n", 
    pf->send_count, pf->recv_count, pf->timeout_count, pf->resp_time, (float)pf->total_time/pf->recv_count, pf->total_time);
  #endif
if (pf->send_count == pingLastCount) { pingOnEnd(pf); } else pingLastCount = pf->send_count; return ESP_OK; };
void pingOnEnd(esp_ping_found * pf) { // Расситываем потери и среднее время float pingLoss = 100; float pingAvgTime = 0; if(pf->recv_count > 0) { pingLoss = (pf->send_count - pf->recv_count) * 100 / pf->send_count; pingAvgTime = (float)pf->total_time/pf->recv_count; };
bool pingerPing(const char* hostName, const int count) { IPAddress hostIP; ip4_addr_t pingTarget; struct sockaddr_in address;
WiFi.hostByName(hostName, hostIP); address.sin_addr.s_addr = hostIP; pingTarget.addr = address.sin_addr.s_addr; pingLastCount = 0; uint32_t pingCount = count; uint32_t pingTimeout = 3000; uint32_t pingDelay = 500;
esp_ping_set_target(PING_TARGET_RES_RESET, NULL, 0); esp_ping_set_target(PING_TARGET_RCV_TIMEO, &pingTimeout, sizeof(uint32_t)); esp_ping_set_target(PING_TARGET_DELAY_TIME, &pingDelay, sizeof(uint32_t)); esp_ping_set_target(PING_TARGET_IP_ADDRESS, &pingTarget, sizeof(uint32_t)); esp_ping_set_target(PING_TARGET_IP_ADDRESS_COUNT, &pingCount, sizeof(uint32_t)); esp_ping_set_target(PING_TARGET_RES_FN, (void*)&pingerOnReceive, 0); ping_init();
return true; }

Ссылки для изучения FreeRTOS:

В процессе поисков по интернету для себя я отобрал следующие ресурсы, которыми пользуюсь постоянно:

  1. Документация по ESP-IDF
  2. Андрей Курниц “FreeRTOS — операционная система для микроконтроллеров”
  3. FreeRTOS — практическое применение
  4. Справочник по Free RTOS API

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *