ESP32 с FreeRTOS: Начало работы с семафорами (Arduino IDE)
В этом руководстве мы познакомим вас с семафорами FreeRTOS для ESP32 и объясним, как их использовать в Arduino IDE. Семафоры – это сигналы (или флаги), которые позволяют синхронизировать задачи и управлять событиями. Они могут использоваться для указания того, что событие произошло или что ресурс доступен. В отличие от очередей, семафоры не передают данные.
Существует два типа семафоров: бинарные семафоры и счётные семафоры. В этом руководстве мы создадим и рассмотрим два различных примера, чтобы показать, как работают эти два типа семафоров.
Впервые работаете с FreeRTOS? Начните с этого руководства: ESP32 with FreeRTOS (Arduino IDE) — Getting Started Guide: Creating Tasks.
Введение в семафоры
Семафоры – это инструменты сигнализации в FreeRTOS, используемые для координации задач. Они обычно применяются для указания того, что событие произошло или что ресурс доступен. В отличие от очередей, семафоры не передают данные, только «счёт», или «флаг», или «сигнал» (или как угодно это назовите), используемый для запуска действий, когда что-то происходит.
Простой пример работы бинарного семафора
Они позволяют задачам ожидать события, такие как нажатие кнопки или обнаружение движения, или сигнализировать о том, что событие произошло. Это делает их особенно полезными в сценариях, связанных с прерываниями и синхронизацией задач.
Поскольку семафоры не хранят данные, они потребляют меньше памяти, чем очереди, и идеально подходят для лёгкой сигнализации событий между задачами.
Существует два типа семафоров:
Бинарный семафор: сигнализирует о единичном событии. Это инструмент синхронизации, который может быть либо пустым (0), либо полным (1). Это как сигнал, которого задача ожидает перед тем, как продолжить выполнение.
Счётный семафор: отслеживает несколько событий (это может быть одно и то же событие несколько раз). Это как очередь событий до максимального счёта, который вы определяете. В отличие от очередей FreeRTOS, они не переносят данные. Только сигнал синхронизации. Вы лучше поймёте, как это работает, позже на примере.
Основные функции семафоров
Вот некоторые основные функции бинарных и счётных семафоров при использовании ESP32 с Arduino IDE. Далее мы рассмотрим эти функции в практических примерах.
Создание бинарного семафора
Для создания бинарного семафора используйте функцию xSemaphoreCreateBinary(). Она возвращает дескриптор SemaphoreHandle_t в случае успеха или NULL, если создание не удалось.
Создание счётного семафора
Для создания счётного семафора используйте функцию xSemaphoreCreateCounting(). Она возвращает дескриптор SemaphoreHandle_t в случае успеха или NULL, если создание не удалось. В качестве аргумента передайте максимальный счёт.
Взятие семафора (получение из семафора)
Используйте функцию xSemaphoreTake(semaphore, timeout) в задаче для ожидания или взятия семафора. Для бинарного семафора она блокируется до тех пор, пока семафор не станет доступным (состояние 1), устанавливая его в 0 при взятии.
Для счётного семафора она уменьшает счёт, если он больше 0, или блокируется, если счёт равен 0. Параметр timeout указывает, как долго ожидать (в тиках); portMAX_DELAY означает бесконечное ожидание. Это означает, что задача будет заблокирована, пока не появится значение семафора для взятия.
Передача семафора
Для передачи семафора используйте функцию xSemaphoreGive(), если вы находитесь внутри задачи, или xSemaphoreGiveFromISR(), если используется в ISR (функциях обработчиков прерываний).
Для бинарного семафора она устанавливает состояние в 1, разблокируя ожидающую задачу (или игнорируется, если семафор уже равен 1). Для счётного семафора она увеличивает счёт до maxCount, разблокируя ожидающую задачу (игнорируется при достижении maxCount).
Пример 1: Бинарный семафор – переключение светодиода нажатием кнопки
В этом разделе мы создадим простой пример для демонстрации работы бинарного семафора и его реализации в практическом приложении. Этот пример будет переключать светодиод при нажатии кнопки. Для сигнализации о нажатии кнопки мы будем использовать семафор. Одновременно у нас будет другая задача, мигающая светодиодом, чтобы продемонстрировать, что мы можем запускать несколько задач одновременно.
Итак, вот обзор примера:
Мы добавим прерывание к кнопке. При нажатии кнопки соответствующий ISR передаст семафор (установит его в 1).
Есть другая задача, называемая
LEDToggleTask(), которая будет ожидать семафор для переключения состояния светодиода. Когда семафор передаётся из ISR, эта задача выполнится и семафор будет сброшен в 0. Только когда семафор установлен в 1, при нажатии кнопки, эта задача выполнится снова.Одновременно у нас есть другая задача, называемая
LEDBlinkTask(), которая будет увеличивать и уменьшать яркость другого светодиода.
Необходимые компоненты
Вот список компонентов, необходимых для этого примера:
Плата ESP32 на ваш выбор – читайте Best ESP32 Development Boards
2x светодиода
2x резистора 220 Ом или аналогичного номинала
Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!
Схема подключения
Подключите следующую схему:
Красный светодиод подключён к GPIO 2
Синий светодиод подключён к GPIO 4
Кнопка подключена к GPIO 23
Следуйте следующей принципиальной схеме.
Код
Загрузите следующий код в Arduino IDE.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-semaphores-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 BUTTON_PIN 23
#define LED1_PIN 2 // Toggled LED
#define LED2_PIN 4 // Blinking LED
#define DEBOUNCE_DELAY 200
SemaphoreHandle_t buttonSemaphore = NULL;
volatile uint32_t lastInterruptTime = 0;
void IRAM_ATTR buttonISR() {
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime > DEBOUNCE_DELAY) {
BaseType_t higherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(buttonSemaphore, &higherPriorityTaskWoken);
lastInterruptTime = currentTime;
if (higherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
}
void LEDToggleTask(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
bool ledState = false;
for (;;) {
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
ledState = !ledState;
digitalWrite(LED1_PIN, ledState ? HIGH : LOW);
Serial.print("LEDToggleTask: LED1 ");
Serial.println(ledState ? "ON" : "OFF");
}
}
}
void LEDBlinkTask(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println("LEDBlinkTask: LED2 ON");
vTaskDelay(250 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println("LEDBlinkTask: LED2 OFF");
vTaskDelay(250 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
// Defining the button as an interrupt
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
buttonSemaphore = xSemaphoreCreateBinary();
if (buttonSemaphore == NULL) {
Serial.println("Failed to create semaphore!");
while (1);
}
xTaskCreatePinnedToCore(
LEDToggleTask, // Task function
"LEDToggleTask", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDBlinkTask, // Task function
"LEDBlinkTask", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Medium priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
}
Как работает код?
Начнём с определения пинов для кнопки, переключаемого светодиода и мигающего светодиода.
#define BUTTON_PIN 23
#define LED1_PIN 2 // Toggled LED
#define LED2_PIN 4 // Blinking LED
Определим задержку дребезга для кнопки в миллисекундах.
#define DEBOUNCE_DELAY 200
Создадим дескриптор для семафора, называемый buttonSemaphore.
SemaphoreHandle_t buttonSemaphore = NULL;
setup()
Давайте сначала объясним setup(), а затем проанализируем задачи.
Сначала установим кнопку как прерывание и зададим её функцию обратного вызова (ISR). В нашем случае она называется buttonISR.
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
Мы создаём бинарный семафор с помощью функции xSemaphoreCreateBinary() на ранее созданном дескрипторе buttonSemaphore.
buttonSemaphore = xSemaphoreCreateBinary();
if (buttonSemaphore == NULL) {
Serial.println("Failed to create semaphore!");
while (1);
}
Затем мы создаём задачи LEDToggleTask и LEDBlinkTask с разными приоритетами.
xTaskCreatePinnedToCore(
LEDToggleTask, // Task function
"LEDToggleTask", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDBlinkTask, // Task function
"LEDBlinkTask", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Medium priority
NULL, // Task handle
1 // Core ID
);
LEDToggleTask
Задача LEDToggleTask() будет переключать состояние LED1, когда есть значение в семафоре.
void LEDToggleTask(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
bool ledState = false;
for (;;) {
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
ledState = !ledState;
digitalWrite(LED1_PIN, ledState ? HIGH : LOW);
Serial.print("LEDToggleTask: LED1 ");
Serial.println(ledState ? "ON" : "OFF");
}
}
}
Когда задача LEDToggleTask() запускается, она настраивает пин светодиода как выход и начинает бесконечный цикл. Внутри цикла она ожидает семафор с помощью xSemaphoreTake(buttonSemaphore, portMAX_DELAY). portMAX_DELAY означает, что задача будет ждать бесконечно, пока не появится значение в семафоре (пока не будет нажата кнопка).
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
Когда семафор получен, задача переключит состояние светодиода и выведет его в Serial Monitor.
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
ledState = !ledState;
digitalWrite(LED1_PIN, ledState ? HIGH : LOW);
Serial.print("LEDToggleTask: LED1 ");
Serial.println(ledState ? "ON" : "OFF");
LEDBlinkTask
Помимо другой задачи, у нас есть LEDBlinkTask, которая работает независимо и одновременно, мигая светодиодом бесконечно.
void LEDBlinkTask(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println("LEDBlinkTask: LED2 ON");
vTaskDelay(250 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println("LEDBlinkTask: LED2 OFF");
vTaskDelay(250 / portTICK_PERIOD_MS);
}
}
Демонстрация
Загрузите код на вашу плату. После загрузки откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку RST на ESP32, чтобы код начал выполняться.
Светодиод, подключённый к GPIO 4, будет мигать каждые 250 миллисекунд. Нажмите кнопку, чтобы переключить состояние светодиода, подключённого к GPIO 2.
В Serial Monitor вы должны увидеть нечто подобное.
Пример 2: Счётный семафор
В этом разделе мы создадим простой пример для демонстрации работы счётных семафоров. Мы создадим счётный семафор с максимальным счётом 5. Этот семафор будет принимать до 5 нажатий кнопки. Другая задача будет потреблять этот семафор, чтобы мигнуть светодиодом столько раз, сколько значений находится в семафоре. Когда значение потребляется из семафора, можно добавить новое значение.
Вкратце, вот обзор того, как работает проект:
Мы присоединим прерывание к кнопке. При нажатии кнопки обработчик прерывания (ISR) передаст семафор – до максимального счёта 5.
Задача
LEDBlinkTaskбудет ожидать семафор. Каждый раз, когда она его получает, она мигнёт светодиодом. Светодиод мигнёт один раз для каждого счёта, доступного в данный момент в семафоре.Когда
LEDBlinkTaskпотребляет значение из семафора, появляется «место» для нового счёта, добавляемого нажатием кнопки.Одновременно у нас будет другая задача, называемая
LEDFadeTask, которая будет плавно изменять яркость другого светодиода. Эта задача используется для демонстрации возможностей FreeRTOS по управлению многозадачностью.
Необходимые компоненты и схема подключения
Такие же, как в предыдущем примере.
Код
Скопируйте следующий код в Arduino IDE.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-semaphores-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 BUTTON_PIN 23
#define LED1_PIN 2 // Blinking LED
#define LED2_PIN 4 // Fading LED
#define DEBOUNCE_DELAY 200 // debounce for the pushbutton in milliseconds
#define SEMAPHORE_MAX_COUNT 5
SemaphoreHandle_t buttonSemaphore = NULL;
volatile uint32_t lastInterruptTime = 0;
void IRAM_ATTR buttonISR() {
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime > DEBOUNCE_DELAY) {
BaseType_t higherPriorityTaskWoken = pdFALSE;
if (xSemaphoreGiveFromISR(buttonSemaphore, &higherPriorityTaskWoken) == pdTRUE) {
Serial.println("buttonISR: Gave semaphore token");
} else {
Serial.println("buttonISR: Semaphore full");
}
lastInterruptTime = currentTime;
if (higherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
}
void LEDBlinkTask(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
for (;;) {
// Get and print the current semaphore count
UBaseType_t count = uxSemaphoreGetCount(buttonSemaphore);
Serial.print("LEDBlinkTask: Current semaphore count = ");
Serial.println(count);
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
Serial.println("LEDBlinkTask: Blinking LED1 for button press");
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
}
void LEDFadeTask(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
// Fade up (0 to 255)
for (int duty = 0; duty <= 255; duty += 5) {
analogWrite(LED2_PIN, duty);
if (duty % 50 == 0) { // Print every 10th step
Serial.print("LEDFadeTask: Fading LED2, duty=");
Serial.println(duty);
}
vTaskDelay(50 / portTICK_PERIOD_MS);
}
// Fade down (255 to 0)
for (int duty = 255; duty >= 0; duty -= 5) {
analogWrite(LED2_PIN, duty);
if (duty % 50 == 0 || duty == 255) {
Serial.print("LEDFadeTask: Fading LED2, duty=");
Serial.println(duty);
}
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
}
void setup() {
Serial.begin(115200); // Higher baud rate
delay(1000);
Serial.println("Starting FreeRTOS: Counting Semaphore");
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
buttonSemaphore = xSemaphoreCreateCounting(SEMAPHORE_MAX_COUNT, 0);
if (buttonSemaphore == NULL) {
Serial.println("Failed to create semaphore!");
while (1);
}
xTaskCreatePinnedToCore(
LEDBlinkTask, // Task function
"LEDBlinkTask", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDFadeTask, // Task function
"LEDFadeTask", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Lower priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {}
Как работает код?
Этот код довольно похож на предыдущий. Мы рассмотрим только важные секции, связанные со счётным семафором.
Создание счётного семафора
В setup() мы создаём счётный семафор с максимальным счётом 5 (SEMAPHORE_MAX_COUNT), начиная с 0. Мы делаем это с помощью функции xSemaphoreCreateCounting().
buttonSemaphore = xSemaphoreCreateCounting(SEMAPHORE_MAX_COUNT, 0);
if (buttonSemaphore == NULL) {
Serial.println("Failed to create semaphore!");
while (1);
}
Создание задач
Также в setup() мы создаём наши задачи и назначаем их ядру. LEDBlinkTask имеет более высокий приоритет, чем LEDFadeTask.
xTaskCreatePinnedToCore(
LEDBlinkTask, // Task function
"LEDBlinkTask", // Task name
3000, // Stack size
NULL, // Task parameters
2, // Higher priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDFadeTask, // Task function
"LEDFadeTask", // Task name
3000, // Stack size
NULL, // Task parameters
1, // Lower priority
NULL, // Task handle
1 // Core ID
);
ISR кнопки и счётный семафор
При нажатии кнопки выполняется функция buttonISR(). Если мы получаем валидное нажатие кнопки, мы передаём его в счётный семафор. Семафор принимает до пяти счётов. Мы используем ту же функцию, что и в предыдущем примере – xSemaphoreGiveFromISR().
if (xSemaphoreGiveFromISR(buttonSemaphore, &higherPriorityTaskWoken) == pdTRUE) {
Serial.println("buttonISR: Gave semaphore token");
} else {
Serial.println("buttonISR: Semaphore full");
}
LEDBlinkTask и взятие семафора
LEDBlinkTask ожидает бесконечно, пока в семафоре не появится счёт. Когда в семафоре есть счёт, мы берём его и мигаем светодиодом.
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY)) {
Serial.println("LEDBlinkTask: Blinking LED1 for button press");
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
Поскольку у нас счётный семафор, он будет мигать светодиодом столько раз, сколько счётов доступно в данный момент в семафоре.
Каждый раз, когда значение берётся из семафора, появляется новое место для добавления нового счёта (через нажатие кнопки).
Внутри этой задачи мы также выводим текущий счёт семафора, вызывая функцию uxSemaphoreGetCount() и передавая дескриптор семафора в качестве аргумента.
// Get and print the current semaphore count
UBaseType_t count = uxSemaphoreGetCount(buttonSemaphore);
Serial.print("LEDBlinkTask: Current semaphore count = ");
Serial.println(count);
LEDFadeTask
Одновременно у нас есть другая независимая задача, называемая LEDFadeTask, которая просто плавно изменяет яркость другого светодиода.
void LEDFadeTask(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
// Fade up (0 to 255)
for (int duty = 0; duty <= 255; duty += 5) {
analogWrite(LED2_PIN, duty);
if (duty % 50 == 0) { // Print every 10th step
Serial.print("LEDFadeTask: Fading LED2, duty=");
Serial.println(duty);
}
vTaskDelay(50 / portTICK_PERIOD_MS);
}
// Fade down (255 to 0)
for (int duty = 255; duty >= 0; duty -= 5) {
analogWrite(LED2_PIN, duty);
if (duty % 50 == 0 || duty == 255) {
Serial.print("LEDFadeTask: Fading LED2, duty=");
Serial.println(duty);
}
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
}
Демонстрация
Загрузите код на вашу плату. После загрузки откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку RST на ESP32, чтобы код начал выполняться.
Светодиод, подключённый к GPIO 4, будет постоянно плавно изменять яркость.
Нажмите кнопку несколько раз, чтобы мигнуть светодиодом, подключённым к GPIO 2, столько раз, сколько нажатий кнопки в очереди семафора.
В Serial Monitor вы должны увидеть нечто подобное. Счёт семафора будет уменьшаться по мере мигания светодиода (если вы не продолжаете нажимать кнопку).
Заключение
В этом руководстве вы узнали о бинарных и счётных семафорах FreeRTOS и о том, как реализовать их с ESP32, программируемым в Arduino IDE.
Семафоры позволяют нам синхронизировать задачи, сигнализировать о доступности ресурса, о произошедшем событии или о точке в задаче, где другая задача должна начать выполнение.
Мы показали вам два простых примера для демонстрации работы семафоров. Это может быть применено к гораздо более сложным приложениям с множеством задач, передающих и берущих из семафора.
Мы надеемся, что это руководство было полезным для начала реализации программирования FreeRTOS в ваших скетчах ESP32. У нас есть больше руководств из серии FreeRTOS, которые могут вам понравиться:
ESP32 with FreeRTOS (Arduino IDE) — Getting Started Guide: Creating Tasks
ESP32 with FreeRTOS Queues: Inter-Task Communication (Arduino IDE)
ESP32 with FreeRTOS: Software Timers/Timer Interrupts (Arduino IDE)
Чтобы узнать больше об ESP32, обязательно ознакомьтесь с нашими ресурсами:
—
Источник: :doc:`Random Nerd Tutorials — ESP32 with FreeRTOS: Getting Started with Semaphores (Arduino IDE) <../esp32-freertos-semaphores-arduino/index>`