Перейти к содержимому

Библиотека циклов событий FreeRTOS

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

Что вы обычно делаете в своей программе, когда вам требуется узнать о наступлении того или иного события в устройстве, например о подключении к WiFi, и предпринять какие-либо действия? Правильно! Добавляете callback-функцию! Но тут возникают потенциальные проблемы:

  1. Чтобы добавить callback функцию из одного библиотечного модуля на событие другого библиотечного модуля нам нужно явно подключить (include) файл источника события в файл подписчика. В итоге получается мешанина кросс-ссылок с одной библиотеки на другую.
  2. Что вы будете делать, если вам нужно среагировать на событие не в одном модуле, а сразу в нескольких? Единственное, что мне приходит на ум – это организовывать связанный список callback-ов и вызывать их по цепочке. Когда у вас эти callback-и в разных и не связанных друг с другом библиотеках, это превращается в кошмар.
  3. FreeRTOS это многозадачная система. И callback будет вызван в контексте той задачи, которая генерирует исходное событие. Иногда это неприменимо – среагировать на событие должен совершенно другой поток / задача.

Чтобы решить эти проблемы, FreeRTOS предоставляет нам встроенный механизм: циклы событий – Event Loop. Но не следует путать их с EventGroups, которые мы обсуждали в одной из предыдущих статей.

Сразу оговорюсь – это архиудобно в большинстве случаев, но иногда удобнее всё-же использовать старый способ (callbacks), всё зависит от ситации.

Попытаюсь объяснить своими словами, что такое Event Loop. Если вы знакомы с MQTT протоколом, то Event Loop – это такой махонький MQTT сервер внутри операционной системы. Компоненты прошивки и библиотеки могут генерировать и публиковать события-данные в определенные “топики”; а другие компоненты и библиотеки могут подписываться на интересующие их “топики” и получать своевременные уведомления, ничего не зная об источнике события.

Каждое событие идентифицируется двумя признаками: строковым const char* event_base (базовый идентификатор событий) и числовым int32_t event_id (номер события в группе). Кроме этого, к событию можно прикрепить данные произвольной длины void* event_data  – например это может быть код ошибки или причина отключения от сети (важно! при отправке события из обработчиков исключений можно отправить не более 4 байт).


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

  • RE_WIFI_STA_GOT_IP – подключение к WiFi установлено и получен IP-адрес. При получении этого сигнала: запускается MQTT-клиент; запускается сервис PING-а; переводятся в рабочий режим клиенты Telegram, ThingSpeak, OpenMon, NarodMon; системный светодиод меняет режим работы на “Подключено к WiFi
  • RE_WIFI_STA_DISCONNECTED – подключение к WiFi потеряно. При получении этого сигнала: приостанавливаются все сетевые службы, а системный светодиод переводится в режим мигания “Нет WiFi
// Forwarded WIFI events
static const char* RE_WIFI_EVENTS = "REVT_WIFI";

typedef enum {
  RE_WIFI_STA_INIT = 0,
  RE_WIFI_STA_STARTED,
  RE_WIFI_STA_STOPPED,
  RE_WIFI_STA_DISCONNECTED,
  RE_WIFI_STA_GOT_IP,
  RE_WIFI_STA_PING_OK,
  RE_WIFI_STA_PING_FAILED
} re_wifi_event_id_t;

Передавать через циклы событий можно события разных самых типов:

  • Временные интервалы (начало каждой минуты, часа, дня, недели…)
  • Ошибки и сбои сенсоров или устройств
  • Получение внешних команд через telegram
  • Получение OTA-обновления прошивки
  • Подключение и отключение к WiFi, MQTT и другим сервисам
  • Потеря доступа в интернет (проверка посредством пинга)
  • Обновление системного времени через SNTP
  • Нажатия на различные кнопки или изменение уровня на GPIO
  • Изменение внутренних параметров
  • Получение сигнала с приемника 433 MHz
  • используйте свою фантазию

При этом “получатель” события ничего не знает о том, какой поток / задача вызвали это событие.

Удобно? На мой взгляд – очень. Используя этот механизм, мы можем связать в единую систему сборище разнородных задач с разными функциями.


Использование цикла событий

Общая схема работы с циклом событий выглядит так:

  • Запустите цикл событий. Это может быть пользовательский (произвольный) или системный (предопределенный) цикл событий. Не обязательно для произвольных (ваших) событий использовать собственный экземпляр цикла событий – вполне можно обойтись системным циклом для всех задач – и системных (wifi к примеру) и ваших собственных прикладных. Но можно и выделенный экземпляр запустить, если свободная память позволяет. Запустить цикл событий при запуске устройства нужно как можно раньше, чтобы потом не возникало проблем при регистрации обработчиков.
  • Заинтересованные задачи должны зарегистрировать обработчики событий, на которые они должны реагировать. Обработчики событий – это те же самые callback-функции, но выполняются они из контекста цикла событий. Поэтому, как и в случае с прерываниями, не стоит включать в обработчики событий слишком “тяжелый” и медленный код, так как это повлияет на диспетчеризацию других событий. Обратите внимание: обработчики событий “пользуются” стеком задачи-цикла событий, а не задачи, генерирующей событие.
  • Отправляем событие в цикл. Задача цикла событий принимает его из входящей очереди и начинает проверку зарегистрированных на втором этапе обработчиков. Если таковой находится, он выполняется.

Вот и всё, ничего сложного. Как видите – цикл событий это просто централизованный способ регистрации и вызова callback-ов, значительно повышающих их удобство для программиста. Вам не требуется заботится, сколько ещё библиотек или модулей ждут этого же события. Вам не требуется включать библиотеку-источник события в список include. Вы можете в любой момент времени отменять подсписку на события и возобновлять её.


Создание цикла событий

Цикл событий – это обычная задача FreeRTOS с входящей очередью, которая при появлении в входящем потоке каких-либо событий перенаправляет эти данные всем заинтересованным “подписчикам”, то есть вызывает зарегистрированные обработчики событий.

Как я уже упоминал, вы можете запустить системный цикл событий или свой собственный. Разницы по функционалу между ними нет никакой, разве что для функций отправки события добавляется суффикс “_to” и требуется указать хэндл цикла. Системный цикл событий используется некоторыми встроенными библиотеками ESP-IDF, в частности “esp_wifi”, и вам придется запускать его в любом случае, если вы будете подключать ESP32 к WiFi.

Нужно ли создавать дополнительный, прикладной, цикл? Не уверен. Вообще у меня в прошивке такая возможность заложена и используется, чтобы не тормозить системные события. Но однажды, после очередной правки кода я допустил ошибку (макрос, управляющий этим, был случайно отключен) и дополнительный “прикладной” цикл не создавался, и абсолютно все события “пошли” через системный цикл событий. И всё прекрасно работало около месяца, без сбоев, почти на всех устройствах (кроме охранно-пожданой сигнализации, где генерировалось очень много внешних событий, и системная очередь изредка просто переполнялась).

Для создания системного цикла событий используйте функцию:

esp_err_t esp_event_loop_create_default(void)

Ей не нужно передавать никаких параметров – всё, что нужно (размер стека, например), конфигурируется через SdkConfig. Проверьте код возврата на предмет ошибок и всё, больше ничего делать не требуется.

Если вы посмотрите примеры для работы с WiFi, то в каждом из них вы найдете эту самую функцию, так как для работы WiFi системный цикл событий требуется обязательно.

Для создания пользовательского цикла событий используйте другую функцию:

esp_err_t esp_event_loop_create(const esp_event_loop_args_t *event_loop_argsesp_event_loop_handle_t *event_loop)

Здесь уже немного сложнее. Во первых вам придется передать в функцию указатель на структуру, в которой вы сами определяете параметры задачи цикла: ядро, приоритет, наименование задачи, а так же размер стека и очереди. А во вторых – передать указатель на хендл цикла событий.

Пример создания такого цикла, взятый из моего “рабочего” кода:

// Create the event loop 
esp_event_loop_args_t _loopCfg;
memset(&_loopCfg, 0, sizeof(_loopCfg));
_loopCfg.queue_size = CONFIG_EVENT_LOOP_QUEUE_SIZE;
_loopCfg.task_name = eventLoopTaskName;
_loopCfg.task_priority = CONFIG_EVENT_LOOP_PRIORITY;
_loopCfg.task_stack_size = CONFIG_EVENT_LOOP_STACK_SIZE;
_loopCfg.task_core_id = CONFIG_EVENT_LOOP_CORE;
esp_err_t err = esp_event_loop_create(&_loopCfg, &_eventLoop);
if (err == ESP_OK) {
  rloga_i("Dedicated event loop created successfully");
  return true;
} else {
  rloga_e("Failed to create event loop!");
};

Так-с, очередь создали, теперь нужно настроить подписки….


Регистрация обработчиков событий

Для создания обработчика вам нужно подготовить функцию следующего прототипа:

void run_on_event(void* handler_argesp_event_base_t event_baseint32_t event_idvoid* event_data);

event_baseevent_id и event_data у вас не должно быть вопросов – про это я уже упоминал выше. Но откуда тут вдруг взялся какой-то handler_arg? Скоро узнаем…

Обработчик можно создать один на несколько разных типов, тогда внутри его вам нужно проверить event_id на соответствие требуемому. Ниже приведён пример обработчика, который при подключении к MQTT генерирует топики сенсоров, а при отключении – удаляет их из памяти. Здесь же используется и пересылаемая структура, в которой содержаться все необходимые данные:

static void sensorsMqttEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
  if (event_id == RE_MQTT_CONNECTED) {
    re_mqtt_event_data_t* data = (re_mqtt_event_data_t*)event_data;
    sensorsMqttTopicsCreate(data->primary);
  } 
  else if ((event_id == RE_MQTT_CONN_LOST) || (event_id == RE_MQTT_CONN_FAILED)) {
    sensorsMqttTopicsFree();
  }
}

В обработчике вам не нужно заботиться об удалении данных event_data – библиотека сделает это сама после завершения всех обработчиков. Только если вы не передаете в event_data “вручную” выделенную память – тогда сами ломайте голову, как и когда её освобождать.

Теперь осталось его только зарегистрировать. Делается это с помощью функций 

esp_err_t esp_event_handler_register(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void *event_handler_arg)

для системного цикла или 

esp_err_t esp_event_handler_register_with(esp_event_loop_handle_t event_loop, esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void *event_handler_arg)

для произвольного цикла.

Функции очень похожи, только в втором случае дополнительно нужно передать хендл цикла, который мы получили на этапе создания цикла событий.

Параметры:

  • event_loop — [in] цикл событий, в котором регистрируется эта функция обработчика. Только для esp_event_handler_instance_register_with()
  • event_base — [in] базовый идентификатор события. Здесь есть тонкость – если указать ESP_EVENT_ANY_BASE, то обработчик будет реагировать на вообще любые события.
  • event_id – [in] идентификатор события. Здесь так же можно указать ESP_EVENT_ANY_ID в качестве идентификатора события, тогда обработчик будет вызван для любого события указанной выше группы.
  • event_handler — [in] функция-обработчик, которая вызывается при отправке события
  • event_handler_arg — [in] данные, помимо данных события, которые передаются обработчику при его вызове. Вот эти данные и будут переданы в обработчик при его вызове в качестве первого аргумента.

Приведу пример из моего рабочего кода:

bool eventHandlerRegister(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void* event_handler_arg)
{
  #if CONFIG_EVENT_LOOP_DEDICATED
    // Dedicated event loop
    if (_eventLoop) {
      rlog_d(logTAG, "Register dedicated event handler for %s #%d", event_base, event_id);
      esp_err_t err = esp_event_handler_register_with(_eventLoop, event_base, event_id, event_handler, event_handler_arg);
      if (err != ESP_OK) {
        rlog_e(logTAG, "Failed to register event handler for %s #%d", event_base, event_id);
        return false;    
      };
    } else {
      return false;
    };
  #else
    // Default event loop
    rlog_d(logTAG, "Register default event handler for %s #%d", event_base, event_id);
    esp_err_t err = esp_event_handler_register(event_base, event_id, event_handler, event_handler_arg);
    if (err != ESP_OK) {
      rlog_e(logTAG, "Failed to register event handler for %s #%d", event_base, event_id);
      return false;    
    };
  #endif // CONFIG_EVENT_LOOP_DEDICATED
  return true;
}

В зависимости от значения CONFIG_EVENT_LOOP_DEDICATED = 1 или 0 будет зарегистрирован обработчик либо для выделенного цикла, либо для системного.

И так повторяем для “всех заинтересованных сторон“…


Генерация событий

Всё готово, можно начинать отправлять события в цикл. Для этого используйте функцию esp_event_post() для системного цикла или esp_event_post_to() для произвольного цикла. Опять здесь разница только в суффиксе “_to” и необходимости указания хендла цикла, куда отправляем известие о событии.

esp_err_t esp_event_post_to(esp_event_loop_handle_t event_loopesp_event_base_t event_baseint32_t event_idconst void *event_datasize_t event_data_sizeTickType_t ticks_to_wait)

Параметры:

  • event_loop — [in] цикл событий, к который пуляем извещение. Только для esp_event_post_to()
  • event_base — [in] базовый идентификатор события
  • event_id – [in] идентификатор события
  • event_data — [in] данные, относящиеся к событию, которое передается обработчику. Сюда проще всего передать указатель на обычную, не динамическую переменную (то есть не размещенную вручную в куче). Цикл событий снимет с нее копию и передаст всем обработчикам, поэтому даже если эта переменная уйдет из зоны видимости, и будет уничтожена компилятором – ничего страшного не произойдет. Но вот передавать здесь указатели на строки или массивы в куче – гораздо хлопотнее – ведь цикл клонирует только собственно указатель и вы сами должны позаботиться об освобождении памяти, иначе будет утечка памяти. А обработчиков теоретически может быть несколько…
  • event_data_size — [in] размер данных события. С этим всё должно быть предельно ясно – sizeof(...).
  • ticks_to_wait — [in] количество тиков для блокировки в полной очереди событий. В принципе, можно просто передать portMAX_DELAY (ждать “до победного”) и не париться. Но в таком случае можно нарваться на непонятные зависания, когда очередь вдруг переполнится.

Я сделал так:

bool eventLoopPost(esp_event_base_t event_base, int32_t event_id, void* event_data, size_t event_data_size, TickType_t ticks_to_wait)
{
  esp_err_t err = ESP_OK;
  #if CONFIG_EVENT_LOOP_DEDICATED
    // Dedicated event loop
    if (!_eventLoop) return false;
    do {
      esp_err_t err = esp_event_post_to(_eventLoop, event_base, event_id, event_data, event_data_size, ticks_to_wait == portMAX_DELAY ? CONFIG_EVENT_LOOP_POST_DELAY : ticks_to_wait);
      if (err != ESP_OK) {
        rlog_e(logTAG, "Failed to post event to \"%s\" #%d: %d (%s)", event_base, event_id, err, esp_err_to_name(err));
        if (ticks_to_wait == portMAX_DELAY) {
          vTaskDelay(CONFIG_EVENT_LOOP_POST_DELAY);
        };
      };
    } while ((ticks_to_wait == portMAX_DELAY) && (err != ESP_OK));
  #else
    // Default event loop
    do {
      esp_err_t err = esp_event_post(event_base, event_id, event_data, event_data_size, ticks_to_wait == portMAX_DELAY ? CONFIG_EVENT_LOOP_POST_DELAY : ticks_to_wait);
      if (err != ESP_OK) {
        rlog_e(logTAG, "Failed to post event to \"%s\" #%d: %d (%s)", event_base, event_id, err, esp_err_to_name(err));
        if (ticks_to_wait == portMAX_DELAY) {
          vTaskDelay(CONFIG_EVENT_LOOP_POST_DELAY);
        };
      };
    } while ((ticks_to_wait == portMAX_DELAY) && (err != ESP_OK));
  #endif // CONFIG_EVENT_LOOP_DEDICATED
  return err == ESP_OK;
}


Генерация событий из прерываний

Ну и напоследок стоит упомянуть об особенностях отправки событий из прерываний. Указанные выше функции esp_event_post() и esp_event_post_to() нельзя использовать из ISR (обработчиков прерываний), так как это гарантированно приведет к срабатыванию WDT таймера и немедленной перезагрузке процессора. Вместо них следует использовать функции esp_event_isr_post() или esp_event_isr_post_to(). Эти версии немногим отличаются от предыдущих версий, ознакомьтесь с ними самостоятельно, кликнув по ссылкам.

Я бы хотел только отметить одну особенность, на которую можно легко “нарваться с наскоку”: размер пересылаемых данных через event_data в данном случае не может превышать 4 байт.


Грабли – наше всё!

Этого обычно нет в официальных инструкциях, и, по идее, должно быть понятно опытному программисту. Но для начинающих наступить на грабли – святое дело, сам так делал поначалу. Поэтому опишу свой опыт – возможно он поможет вам их миновать.

Как, надеюсь, вам стало понятно из статьи – обработчики событий всегда выполняются из контекста задачи – цикла событий. То есть обработчики располагаются в стеке этого самого цикла. И если микроконтроллер стал вдруг перезагружаться по причине переполнения стека, а адрес указывает на обработчик события – добавлять стек нужно для задачи цикла событий.

Во вторых, во время появления нового события обработчики вызываются последовательно, один за другим. Поэтому стоит обращать внимание на то, что зависание в одном из обработчиков вызовет полную блокировку всех остальных обработчиков, да и всего цикла в целом.

При отправке события в цикл можно указать время ожидания ticks_to_wait равным portMAX_DELAY (бесконечно), но если при этом очередь цикла событий окажется переполненной, поток, отправляющий событие зависнет на определенное время до появления свободного “окна”. И вы не сразу поймете в чем дело.


Полезные ссылки

  1. ESP-IDF :: Event Loop Library
  2. Моя библиотека – обертка

 

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

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