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

Библиотека циклов событий 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. Моя библиотека – обертка

 


Пожалуйста, оцените статью:
[ 5 из 5, всего 1 оценок ]

3 комментария для “Библиотека циклов событий FreeRTOS”

  1. Спасибо за цикл очень полезных статей. Хочу добавить свой опыт использование циклов и аргументировать, почему имеет смысл создавать пользовательский цикл.
    Исходная задача: по нажатию кнопки нужно было приконнектится к WiFi. В качестве реализации выбрал путь: interrupt кнопки -> debounce timer -> esp_event_post. И где-то в недрах программы стоит esp_event_handler, который ловит этот эвент и стартует WiFi. Не работает! Путем разбирательств обнаружил почему: в обработчике эвента запускается WiFi, который, в свою очередь тоже имеет обработчик events из default loop. А этому обработчику не дают запуститься, потому что еще не отработал предыдущий. Кусаем самого себя за хвост.
    Создание пользовательского loop решило проблему. Но, надо помнить, что ты можешь из обработчика вызывать какие-то системные методы, про которые, даже не подозреваешь, что они тоже пользуются эвентами.

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

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