ESP32 FreeRTOS Mutex – Начало работы (Arduino IDE)
В этом руководстве вы узнаете, как использовать мьютекс (Mutex) FreeRTOS на ESP32, программируемом в Arduino IDE. Мьютекс (Mutual Exclusion – взаимное исключение) – это особый тип бинарного семафора, который контролирует доступ к общим ресурсам между двумя или более задачами. Это гарантирует, что только одна задача может получить доступ к критическому ресурсу в определённый момент времени. Мы объясним основы мьютекса и покажем простой пример его реализации.
Новичок в FreeRTOS? Начните с этого руководства: ESP32 с FreeRTOS (Arduino IDE) – Руководство по началу работы: Создание задач.
Введение в мьютексы
Мьютекс – это особый тип бинарного семафора (см. наше руководство по семафорам), используемый для контроля доступа к ресурсу, общему для двух или более задач. Он создаётся с помощью xSemaphoreCreateMutex() и гарантирует, что только одна задача может использовать ресурс в определённый момент времени.
Термин «mutex» означает «MUTual EXclusion» (взаимное исключение), и он работает как токен, который позволяет задаче, владеющей этим токеном, безопасно обращаться к общему ресурсу. Чтобы использовать мьютекс, задача должна сначала «взять» его с помощью xSemaphoreTake(mutex, timeout), чтобы стать владельцем токена и войти в критическую секцию (код, обращающийся к ресурсу). Когда задача завершает работу, она «возвращает» мьютекс с помощью xSemaphoreGive(mutex), разблокируя его для других задач. Только после этого другая задача может взять мьютекс и получить доступ к ресурсу без конфликтов.
На следующей диаграмме показано, как это работает.
Использование мьютекса для защиты доступа к общему ресурсу (Источник изображения: freertos.org)
Сценарии использования на ESP32
Например, представьте сценарий, в котором две задачи выполняют HTTP-запросы для сохранения данных в базу данных. Без мьютекса обе задачи могут попытаться обратиться к базе данных и записать данные одновременно, рискуя повредить данные, потому что одна задача начинает новый HTTP-запрос до того, как другая завершит свой. С мьютексом вы можете гарантировать, что только одна задача обращается к базе данных в определённый момент времени, обеспечивая безопасные последовательные операции.
Эта концепция применима ко многим сценариям, таким как совместный доступ к Serial порту несколькими задачами, шина I2C для датчиков и дисплеев, несколько задач, читающих данные с одного датчика, несколько задач, записывающих данные на один дисплей и т.д.
Мьютекс и наследование приоритетов
В FreeRTOS, когда задача владеет мьютексом, она сохраняет контроль над общим ресурсом до тех пор, пока не освободит токен. Если задача с низким приоритетом владеет мьютексом, а задача с высоким приоритетом пытается его взять, задача с высоким приоритетом будет вынуждена ждать. Эта ситуация называется инверсией приоритетов, потому что задача с низким приоритетом фактически блокирует задачу с более высоким приоритетом.
Для решения этой проблемы FreeRTOS реализует наследование приоритетов. Когда задача с высоким приоритетом ожидает мьютекс, удерживаемый задачей с более низким приоритетом, задача с низким приоритетом временно «наследует» более высокий приоритет. Это позволяет ей завершить свою работу и освободить мьютекс быстрее, сокращая задержку для задачи с высоким приоритетом. После освобождения мьютекса приоритет задачи возвращается к исходному уровню.
Итого, в сводке:
Инверсия приоритетов происходит, когда задача с низким приоритетом удерживает мьютекс и блокирует задачу с высоким приоритетом, которая в нём нуждается.
Задача с высоким приоритетом не может выполняться, пока задача с низким приоритетом не освободит мьютекс.
FreeRTOS использует наследование приоритетов для решения этой проблемы.
Задача с низким приоритетом временно наследует высокий приоритет.
Это позволяет ей завершить работу быстрее и освободить мьютекс раньше.
После освобождения приоритет задачи возвращается к нормальному.
Наконец, ещё одна важная особенность мьютексов заключается в том, что их не следует использовать из прерывания, потому что (цитируя документацию FreeRTOS):
Они включают механизм наследования приоритетов, который имеет смысл только если мьютекс берётся и отдаётся из задачи, а не из прерывания.
Прерывание не может блокироваться в ожидании ресурса, защищённого мьютексом.
Примеры применения мьютексов в проектах на ESP32
Использование мьютексов FreeRTOS может быть полезным для ваших IoT-проектов на ESP32. Например:
Общие данные датчиков: например, нескольким задачам нужно читать или обновлять одни и те же значения датчиков.
Вывод на дисплей: нескольким задачам нужно выводить данные в Serial Monitor или на дисплей. Необходимо обеспечить, чтобы только одна задача выводила данные в определённый момент, чтобы их выводы не накладывались друг на друга. Мы рассмотрим этот пример в данном руководстве.
Операции Wi-Fi: если разные задачи выполняют HTTP-запросы, публикации MQTT или WebSocket-коммуникацию, мьютекс предотвращает одновременный доступ к одному сетевому ресурсу, избегая ошибок подключения или повреждения базы данных.
Логирование данных на SD-карту или LittleFS: запись во флеш-память или на SD-карту должна выполняться аккуратно, чтобы избежать повреждения данных. С мьютексом мы гарантируем, что одна задача завершит операцию до того, как другая сможет начать.
Основные функции мьютексов
Вот основные функции для создания и работы с мьютексом при использовании ESP32 с Arduino IDE. Далее мы рассмотрим эти функции на практическом примере.
Создание мьютекса
Для создания мьютекса используйте xSemaphoreCreateMutex(). Функция возвращает дескриптор SemaphoreHandle_t или NULL, если создание не удалось.
Взятие мьютекса
Чтобы взять мьютекс (и получить доступ к общему ресурсу), используйте xSemaphoreTake(mutex, timeout). Параметр timeout – это максимальное время, в течение которого задача будет ожидать доступности мьютекса в заблокированном состоянии. Используйте portMAX_DELAY в качестве timeout, если хотите, чтобы задача ожидала бесконечно.
Освобождение мьютекса
Чтобы отдать мьютекс (освободить его, чтобы другие задачи могли получить доступ к общему ресурсу), используйте xSemaphoreGive(mutex), разблокируя ожидающую задачу. Эту функцию всегда следует вызывать после критической секции для освобождения ресурса.
Пример мьютекса на ESP32: общий Serial вывод
Чтобы показать, как можно использовать мьютекс с ESP32 и как он работает, мы рассмотрим простой пример с общим Serial выводом. Мы протестируем код с мьютексом и без него, чтобы вы увидели разницу.
Вот пример, который мы запустим:
Мы создадим две разные задачи.
Каждая задача выведет две строки текста в Serial Monitor с небольшой задержкой между строками.
Задачи выполняются с разными интервалами, поэтому их выводы могут наложиться друг на друга.
Мы не хотим, чтобы наложение произошло.
Используя мьютекс, мы гарантируем, что каждая задача завершит вывод своих двух строк до того, как другая задача начнёт запись.
Сначала мы запустим пример без мьютекса. Затем запустим его снова с мьютексом, чтобы показать разницу.
Общий Serial вывод (без мьютекса)
Загрузите следующий код на вашу плату ESP32.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-mutex-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
void Task1(void *parameter) {
for (;;) {
Serial.println("Task1: Logging from Task 1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Task1: End of log from Task 1");
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
void Task2(void *parameter) {
for (;;) {
Serial.println("Task2: Logging from Task 2");
vTaskDelay(800 / portTICK_PERIOD_MS);
Serial.println("Task2: End of log from Task 2");
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Starting FreeRTOS");
xTaskCreatePinnedToCore(
Task1, // Task function
"Task1", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
}
Этот код создаёт две задачи. Обе записывают две строки текста в Serial Monitor.
Вот одна задача:
void Task1(void *parameter) {
for (;;) {
Serial.println("Task1: Logging from Task 1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Task1: End of log from Task 1");
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
А вот другая:
void Task2(void *parameter) {
for (;;) {
Serial.println("Task2: Logging from Task 2");
vTaskDelay(800 / portTICK_PERIOD_MS);
Serial.println("Task2: End of log from Task 2");
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
Теперь, после загрузки кода, нажмите кнопку RST на ESP32 и откройте Serial Monitor.
Вы увидите, что одна задача может выводить данные, пока другая ещё работает, из-за чего их выводы накладываются друг на друга. Этот пример демонстрирует, что может произойти в аналогичных ситуациях, таких как вывод данных на дисплей или запись в базу данных. Чтобы предотвратить это, мы можем использовать мьютекс – см. следующий пример.
Общий Serial вывод (с мьютексом)
Этот пример работает так же, как предыдущий, но теперь мы используем мьютекс, чтобы одна задача не могла работать, пока другая не освободит его.
Вот код для загрузки на ESP32:
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-mutex-arduino/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define MUTEX_TIMEOUT 5000 // 5s timeout
SemaphoreHandle_t serialMutex = NULL;
void Task1(void *parameter) {
for (;;) {
if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
Serial.println("Task1: Logging from Task 1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Task1: End of log from Task 1");
xSemaphoreGive(serialMutex);
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
void Task2(void *parameter) {
for (;;) {
if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
Serial.println("Task2: Logging from Task 2");
vTaskDelay(800 / portTICK_PERIOD_MS);
Serial.println("Task2: End of log from Task 2");
xSemaphoreGive(serialMutex);
}
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Starting FreeRTOS");
serialMutex = xSemaphoreCreateMutex();
if (serialMutex == NULL) {
Serial.println("Failed to create mutex!");
while (1);
}
xTaskCreatePinnedToCore(
Task1, // Task function
"Task1", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
}
Как работает код?
Мы начинаем с определения таймаута для мьютекса. Это максимальное время, в течение которого задача будет ожидать мьютекс, прежде чем вернёт pdFALSE.
#define MUTEX_TIMEOUT 5000 // 5s timeout
Создаём дескриптор для мьютекса. Это тот же тип дескриптора, который используется с обычным семафором. Мы называем его serialMutex.
SemaphoreHandle_t serialMutex = NULL;
В setup() мы создаём мьютекс с помощью функции xSemaphoreCreateMutex(), как было описано во вводной части.
serialMutex = xSemaphoreCreateMutex();
if (serialMutex == NULL) {
Serial.println("Failed to create mutex!");
while (1);
}
Также в setup() мы создаём наши задачи.
xTaskCreatePinnedToCore(
Task1, // Task function
"Task1", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
Теперь давайте рассмотрим callback-функции задач, определённые до setup(). Ниже показана Task1, и здесь мы можем увидеть, как используется мьютекс. Он использует тот же API, что и семафоры.
void Task1(void *parameter) {
for (;;) {
if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
Serial.println("Task1: Logging from Task 1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Task1: End of log from Task 1");
xSemaphoreGive(serialMutex);
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
Сначала мы проверяем, можем ли мы получить мьютекс с помощью xSemaphoreTake(). Функция будет ожидать максимальное время MUTEX_TIMEOUT.
if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
Если можем, мы выводим строки в Serial Monitor, то есть входим в критическую секцию (общий ресурс).
Serial.println("Task1: Logging from Task 1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Task1: End of log from Task 1");
Сразу после вывода, поскольку мы уже завершили критическую секцию (в данном случае – доступ к Serial Monitor), мы можем освободить мьютекс с помощью xSemaphoreGive(serialMutex).
xSemaphoreGive(serialMutex);
В случае, если MUTEX_TIMEOUT превышен при ожидании мьютекса, выполнение перейдёт к следующей строке, прежде чем снова проверить мьютекс.
vTaskDelay(5000 / portTICK_PERIOD_MS);
Callback для Task2 работает аналогично, но с другими таймингами.
void Task2(void *parameter) {
for (;;) {
if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
Serial.println("Task2: Logging from Task 2");
vTaskDelay(800 / portTICK_PERIOD_MS);
Serial.println("Task2: End of log from Task 2");
xSemaphoreGive(serialMutex);
}
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
Демонстрация
Загрузите код на ESP32. Затем откройте Serial Monitor и нажмите кнопку RST на ESP32, чтобы код начал выполняться.
Обратите внимание, что на этот раз вторая задача начинает работу только после того, как первая завершила вывод данных. Мьютекс предотвращает наложение задач друг на друга. Обе задачи хотят получить доступ к Serial Monitor, но только задача, владеющая мьютексом, может писать в него.
Заключение
Подводя итог, использование мьютексов – это простой, но мощный способ контроля доступа к общим ресурсам в ваших проектах на ESP32. Независимо от того, выводите ли вы данные в Serial Monitor, обновляете дисплей, записываете в файл, отправляете данные по Wi-Fi, обновляете базу данных и т.д., мьютексы помогают избежать конфликтов между задачами в вашем коде.
Для получения дополнительной информации о мьютексах FreeRTOS вы можете ознакомиться с официальной документацией.
Надеемся, что это руководство оказалось полезным. Если вы хотите узнать больше о программировании FreeRTOS на ESP32 в Arduino IDE, ознакомьтесь с другими руководствами из этой серии:
ESP32 с FreeRTOS (Arduino IDE) – Руководство по началу работы: Создание задач
ESP32 с FreeRTOS Очереди: Межзадачная коммуникация (Arduino IDE)
ESP32 с FreeRTOS: Программные таймеры / Прерывания таймеров (Arduino IDE)
Как использовать двухъядерность ESP32 с Arduino IDE (FreeRTOS)
Чтобы узнать больше о ESP32, ознакомьтесь с нашими ресурсами:
Источник: Rui Santos & Sara Santos – Random Nerd Tutorials