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

Добрый день, уважаемый читатель! Сегодня поговорим об ещё одном очень полезном объекте FreeRTOS – группах событий или EventGroup.

Принцип работы группы событий

Группа событий — это набор бит, которым ваше приложение может присваивать значение “0” (бит не установлен) или “1” (бит установлен). На первый взгляд, того же самого можно достичь, просто устанавливая отдельные биты в целом числе, но есть большое “но” – битовые операции не годятся для многопоточных систем.

Лично на мой взгляд, это не очень удачное название, правильнее было бы “группа состояний” или “группа флагов”, но не я это придумал…

Кроме функции потокобезопасного переключения событий, группы событий несут в себе ещё одну очень важную функцию – перевод задачи в состояние ожидания тех или иных событий. Группы событий позволяют одновременно различным задачам ожидать одного или сразу нескольких событий в заблокированном (приостановленном) состоянии. Когда ожидаемое событие таки происходит, оно разблокирует сразу все ожидающие его задачи. Этим группы отличаются от очередей – в случае с очередью разблокирована будет только одна задача. Иногда так же полезно заменить несколько семафоров одной группой событий, чтобы уменьшить использование оперативной памяти.

Варианты практического применения

Группы событий ну очень удобно использовать для хранения различных состояний устройства:

  • есть ли подключение к WiFi роутеру
  • состояние ping-а “до интернета”
  • подключение к MQTT-брокеру
  • подключение к внешним сервисам
  • работает или нет синхронизация времени, либо установлены аппаратные часы
  • состояние подключенных сенсоров и датчиков
  • и т.д. и т.п.

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

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

 


Емкость группы событий и объявление событий

Группа событий может хранить от 1 до 24 бит (состояний) – это биты от BIT0 до BIT23. “Внутри”, для физического хранения состояний используется как минимум 32-битное число, но старшие биты от 24 до 31 зарезервированы под “служебные надобности” и вы не можете их использовать.

Для объявления флагов (другими словами: состояний, бит) очень удобно использовать предопределенные макросы BIT0BIT1 ~ BIT24 из esp_bit_defs.h, но вполне можно использовать и побитовый сдвиг: 1<<01<<1 и т.д.:

// Изменение уровня воды
#define WATER_LEVEL_CHANGED   BIT0
#define WATER_LEVEL_LOW       BIT1

// Перелив
#define WATER_LEAK_IN1        BIT2
#define WATER_LEAK_IN2        BIT3
#define WATER_LEAK_IN3        BIT4

// Временные события
#define TIME_MINUTE_EVENT     BIT8

// Есть изменения на любом из входов
#define FORCED_CONTROL        (WATER_LEVEL_CHANGED | TIME_MINUTE_EVENT)

Проверить состояние бита можно в любой момент, но вот ожидать задачи могут только установку флага, поэтому если вам потребуется в вашей программе дождаться “сброса” какого-либо флага (установки в состояние “0”), этот флаг придется инвертировать. 

 


Создание и удаление группы событий

Как и для большинства других объектов FreeRTOS, прежде чем начать работу с группой событий, её необходимо создать и получить хендл (указатель) на неё. Это делается с помощью двух функций:

  • EventGroupHandle_txEventGroupCreate(void) – динамическое создание группы с выделением буфера под хранение данных из общей оперативной памяти (кучи). Никаких параметров в этом случае передавать не требуется, “в ответ” мы получим хендл группы событий. Обязательно проверьте его на NULL – если внутри xEventGroupCreate() не удастся выделить память под данные группы, иначе вы почти гарантировано нарветесь на исключение и перезагрузку контроллера.
  • EventGroupHandle_txEventGroupCreateStatic(StaticEventGroup_t *pxEventGroupBuffer) – создание группы с использованием статического буфера StaticEventGroup_t. Этот способ чуть сложнее, но позволяет заранее (на этапе компиляции) выделить необходимый общем памяти под группу событий и, тем самым, уменьшить фрагментацию кучи. xEventGroupCreateStatic() в любом случае вернет действующий хэндл (ведь память фактически уже была выделена заранее, её нужно только подобающим образом “оформить”), поэтому проверка на NULL в данном случае не требуется. Пример ниже иллюстрирует оба способа, в зависимости от предварительно заданного макроса препроцессора:
static EventGroupHandle_t _wateringFlags = nullptr;

#if CONFIG_WATERING_STATIC_ALLOCATION
  static StaticEventGroup_t wateringFlagsBuffer;
  _wateringFlags = xEventGroupCreateStatic(&wateringFlagsBuffer);
#else
  _wateringFlags = xEventGroupCreate();
#endif // CONFIG_WATERING_STATIC_ALLOCATION

 

Удаление группы событий

Удаление группы имеет смысл только в случае её динамического создания, так как в случае статического создания особого смысла в этом нет. Сделать это можно с помощью функции vEventGroupDelete(), передав ей хендл ранее созданной группы событий:

void vEventGroupDelete(EventGroupHandle_t xEventGroup)

 


Чтение состояния группы событий

Получить текущее состояние группы (без какого-либо ожидания) можно с помощью простой функции-макроса:

EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup)

Вы передаете этой функции хэндл группы, а в ответ получаете 32-битное число, отражающее текущее состояние всех рабочих бит (с 0 по 23) группы. Затем, с помощью битовых операций вы должны выяснить, “взведён” нужный вам бит или нет, например так:

// Вариант 1
// Проверяем состояние перелива
if (xEventGroupGetBits(_wateringFlags) & (WATER_LEAK_IN1 | WATER_LEAK_IN2 | WATER_LEAK_IN3)) == WATER_LEAK_IN1) {
  // Перелив по входу 1 (без учета других входов)
};

// Вариант 2
// Проверяем состояние перелива
if (xEventGroupGetBits(_wateringFlags) & (WATER_LEAK_IN1 | WATER_LEAK_IN2 | WATER_LEAK_IN3)) > 0) {
  // Перелив по любому входу
};

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

 


Чтение состояния со сбросом указанных флагов

Если необходимо сбросить некоторые биты сразу после прочтения их текущего статуса, воспользуйтесь другой функцией:

EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroupconst EventBits_t uxBitsToClear)

Эта функция, так же как и xEventGroupGetBits(), вернет вам состояние группы событий перед операцией сброса в виде 32-битного числа, но и дополнительно сбросит те биты, которые вы перечислите в аргументе uxBitsToClear, остальные биты не будут затронуты. Если передать uxBitsToClear = 0, то брюки превращаются, брюки превращаются эта функция плавно превращается в xEventGroupGetBits().

Эту же функцию можно использовать и просто для сброса одного или нескольких флагов, в этом случае вы просто игнорируете возвращаемый результат. За одну операцию можно сбросить сразу несколько флагов / бит, для этого сложите несколько отдельных бит с помощью логического ИЛИ:

xEventGroupGlearBits(_wateringFlags, (WATER_LEAK_IN1 | WATER_LEAK_IN2 | WATER_LEAK_IN3));

 

Важное замечание: xEventGroupClearBits(), как и xEventGroupGetBits(), нельзя использовать из прерываний! Для чтения группы событий и сброса флагов следует воспользоваться другими функциями:

EventBits_t xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup)

или

EventBits_t xEventGroupClearBitsFromISR(xEventGroup, uxBitsToClear)

Более подробная информация о работе с группами событий из прерываний будет чуть ниже.

 


Установка одного или нескольких бит

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

EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroupconst EventBits_t uxBitsToSet)

Вы можете передать через аргумент uxBitsToSet один или сразу несколько бит, объединив их с помощью ИЛИ |:

xEventGroupSetBits(_wateringFlags, (WATER_LEAK_IN1 | WATER_LEAK_IN2 | WATER_LEAK_IN3));

Эту функцию также нельзя использовать из обработчиков прерываний, для работы из прерываний имеется другая версия – xEventGroupSetBitsFromISR().

 


Ожидание одного или нескольких событий

Для ожидания каких-либо событий | бит | флагов в группе существует такая функция:

EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroupconst EventBits_t uxBitsToWaitForconst BaseType_t xClearOnExitconst BaseType_t xWaitForAllBits, TickType_t xTicksToWait)

где:

  • EventGroupHandle_t xEventGroup – указатель на группу событий
  • const EventBits_t uxBitsToWaitFor – один или несколько бит, которые необходимо ожидать, их можно сложить с помощью | (как и в предыдущих случаях)
  • const BaseType_t xClearOnExit – если для xClearOnExit задано значение pdTRUE, то любые биты в uxBitsToWaitFor, установленные в группе событий, будут очищены до выхода из xEventGroupWaitBits(), но только если условие ожидания было выполнено (если функция возвращается по причине, отличной от таймаута). Если для xClearOnExit установлено значение pdFALSE, биты, установленные в группе событий, не изменяются при возврате вызова xEventGroupWaitBits().
  • const BaseType_t xWaitForAllBits – если для xWaitForAllBits задано значение pdTRUE, тогда функция xEventGroupWaitBits() завершится только когда либо будут установлены все биты в uxBitsToWaitFor, либо истечет указанное время xTicksToWait. Если для xWaitForAllBits установлено значение pdFALSE, тогда xEventGroupWaitBits() завершится, когда будет установлен любой из битов, установленных в uxBitsToWaitFor, или по таймауту.
  • TickType_t xTicksToWait – максимальное количество времени (указанное в тиках FreeRTOS) для ожидания установки одного или всех (в зависимости от значения xWaitForAllBits) битов, указанных в uxBitsToWaitFor. Укажите portMAX_DELAY для бесконечного ожидания, либо используйте макросы pdMS_TO_TICKS ( время в мс ) или время в мс / portTICK_PERIOD_MS для пересчета миллисекунд в тики RTOS.

 

Функция вернет состояние группы событий на момент установки ожидаемых битов или истечения времени блока. Обязательно проверьте возвращаемое значение, чтобы узнать, какие биты были установлены. Если функция xEventGroupWaitBits() возвращается из-за истечения времени ожидания, то не все ожидаемые биты будут установлены.

Если функция xEventGroupWaitBits() возвратила значение из-за того, что биты, которых она ожидала, были установлены, то возвращаемое значение является состоянием группы событий до того, как какие-либо биты были автоматически очищены в случае, если для параметра xClearOnExit было установлено значение pdTRUE.

#define WATER_LEVEL_CHANGED   BIT0
#define TIME_MINUTE_EVENT     BIT8
#define FORCED_CONTROL        (WATER_LEVEL_CHANGED | TIME_MINUTE_EVENT)

EventBits_t bits;

bits = xEventGroupWaitBits(
  _wateringFlags,      // Указатель на группу событий
  FORCED_CONTROL,      // Биты, которые ожидаем
  pdTRUE,              // Очистить ожидаемые биты при выходе
  pdFALSE,             // Любой из указанных битов
  pdMS_TO_TICKS(1000)  // Время ожидания
);

if (bits & WATER_LEVEL_CHANGED) {
  ...
};

 


Работа с группой событий из обработчиков прерываний

Чтение состояния группы внутри обработчика прерываний

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

EventBits_t xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup)

Её применение для программиста ничем не отличается от обычной версии – xEventGroupGetBits. Единственное отличие – она должна вызываться только из обработчиков прерываний.

А вот ожидание бит в обработчике прерываний противоречит здравому смыслу.

 

Очистка или установка бит из прерываний

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

Поэтому специальные версии функций xEventGroupClearBitsFromISR(xEventGroup, uxBitsToClear) и xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup) отправляют сообщение задаче таймера, чтобы операция очистки или установки бит выполнялась в контексте задачи таймера. Если запрос на выполнение очистки или установки бит был отправлен в очередь таймера успешно, то возвращается pdPASS, в противном случае возвращается pdFALSE. После этого необходимо принудительно переключить контекст, дабы ожидающие данного бита задачи смогли быстрее его обработать.

Очистка нескольких бит в группе:

#define BIT_0 ( 1 << 0 )
#define BIT_4 ( 1 << 4 )

EventGroupHandle_t xEventGroup;

void anInterruptHandler( void ) 
{
  // Очистка бит 0 и 4 в группе событий xEventGroup
  xResult = xEventGroupClearBitsFromISR(xEventGroup, BIT_0 | BIT_4);

  // Если операция выполнена успешно - переключим контекст
  if (xResult == pdPASS) {
    portYIELD_FROM_ISR(pdTRUE);
  };
}

Установка нескольких бит в группе:

#define BIT_0 ( 1 << 0 )
#define BIT_4 ( 1 << 4 )

EventGroupHandle_t xEventGroup;

void anInterruptHandler( void )
{
  BaseType_t xHigherPriorityTaskWoken, xResult;
  // xHigherPriorityTaskWoken должен предварительно быть установлен в pdFALSE.
  xHigherPriorityTaskWoken = pdFALSE;

  // Установка бит 0 и 4 в группе событий xEventGroup
  xResult = xEventGroupSetBitsFromISR(xEventGroup, BIT_0 | BIT_4, &xHigherPriorityTaskWoken);

  // Сообщение таймеру отправлено
  if (xResult == pdPASS) {
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  }
}

 


 

Демонстрационный проект

Для примера набросал небольшой проект, который состоит из группы событий и одной ожидающей задачи.

#define FLAG_0 (1<<0)
#define FLAG_1 (1<<1)
#define FLAG_2 (1<<2)

// Функция задачи 1
void task1_exec(void *pvParameters)
{
  EventBits_t uxBits;
  while(1) {
    uxBits = xEventGroupWaitBits(
      xEventGroup,               // Указатель на группу событий
      FLAG_0 | FLAG_1 | FLAG_2,  // Какие биты мы ждем
      pdTRUE,                    // Сбросить утановленные биты, после того как они были прочитаны
      pdFALSE,                   // Любой бит (даже один) приведет к выходу из ожидания
      pdMS_TO_TICKS(10000));     // Период ожидания 10 секунд

    // Проверяем, какие биты были установлены
    if ((uxBits & (FLAG_0 | FLAG_1 | FLAG_2)) != 0) {
      // Какой-то бит или несколько был установлен(ы), осталось выяснить какой
      if ((uxBits & FLAG_0) != 0) ESP_LOGI("EventGroups", "FLAG0 is set");
      if ((uxBits & FLAG_1) != 0) ESP_LOGI("EventGroups", "FLAG1 is set");
      if ((uxBits & FLAG_2) != 0) ESP_LOGI("EventGroups", "FLAG2 is set");
    } else {
      // Ни один бит не был установлен
      ESP_LOGI("EventGroups", "Timeout expired");
    };
  };
  vTaskDelete(NULL);
}

Все объекты создаются статическим способом.

void app_main() 
{
  // Создание группы статическим методом
  xEventGroup = xEventGroupCreateStatic(&xEventBuffer);
  // Очищаем все рабочие биты группы
  xEventGroupClearBits(xEventGroup, 0x00FFFFFFU);

  // Запуск задачи статическим методом
  xTaskCreateStaticPinnedToCore(task1_exec, "task1", STACK_SIZE, NULL, 5, xStack, &xTaskBuffer, 1);

  // Демонстрационный цикл
  while (1) {
    // Ждем 3 секунды и устанавливаем бит FLAG_1
    vTaskDelay(pdMS_TO_TICKS(3000));
    xEventGroupSetBits(xEventGroup, FLAG_1);

    // Ждем 2 секунды и устанавливаем бит FLAG_0
    vTaskDelay(pdMS_TO_TICKS(2000));
    xEventGroupSetBits(xEventGroup, FLAG_0);

    // Ждем 4 секунды и устанавливаем биты FLAG_0 и FLAG_2 одновременно
    vTaskDelay(pdMS_TO_TICKS(4000));
    xEventGroupSetBits(xEventGroup, FLAG_0 | FLAG_2);

    // Ждем 20 секунд (для таймамута) и устанавливаем все биты одновременно
    vTaskDelay(pdMS_TO_TICKS(20000));
    xEventGroupSetBits(xEventGroup, FLAG_0 | FLAG_1 | FLAG_2);
  };
}

Скачать проект, как обычно, можно на GitHub

 


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

  1. ESP-IDF :: Event Group API
  2. Пример на GitHub

 

💠 Полный архив статей вы найдете здесь


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

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

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