- PVSM.RU - https://www.pvsm.ru -
Привет!
Это четвертая статья из цикла по ESP-IDF. Как и обещал, сегодня мы рассмотрим мьютексы и семафоры на простых (и не очень) примерах.

Попробую ответить на вопрос, который может возникнуть у многих - а зачем мне вообще изучать семафоры и мьютексы мне вполне для синхронизации хватит очередей?
Смоделируем ситуацию: вы — стажёр в офисе, у вас есть коллега-стажёр Виталя и начальник Колян. В вашем уютном офисе есть почтовый ящик (очередь): кто-то (producer) бросает туда письмо (данные или команду), а кто‑то другой (consumer) его забирает и обрабатывает. Итог:
Почтовый ящик (единственный) на офис, куда стажёры кладут письма. [очередь]
Два стажёра (Вы и Виталя), которые готовят письма и носят их в ящик. [producer]
Начальник (Колян), который иногда проверяет ящик и забирает письма, чтобы их отправить. [consumer]
Почтовый ящик — это механический ящик с замком. У него есть один ключ, и пока кто‑то держит ключ, другие не умеют открыть крышку.
Когда стажёр (Вы) подходит к ящику, он берёт ключ (вызывает xSemaphoreTake(mutex)), открывает крышку, кладёт письмо, закрывает и возвращает ключ (xSemaphoreGive(mutex)).
Если в этот момент второй стажёр (Виталя) пытается открыть ящик, ему придётся ждать, пока Вы вернёте ключ.
Без мьютекса оба стажёра яростно пытались бы залезть в этот ящик и как можно быстрее сбросить туда свои письма. Ящик мог бы зависнуть в полузакрытом состоянии. Мьютекс гарантирует, что Виталя дождётся, когда вы отдадите ему ключ, и не будет пытаться опередить вас.
Кроме того, если у Коляна (начальника) очень важная задача по проверке ящика (высокий приоритет), а тут два стажёра «тупят», весь бизнес накрылся бы медным тазом. Здесь вступает ещё одна фича — priority inheritance. Она повысит приоритет стажёра, держащего ключ (мьютекс), чтобы он быстрее освободил ресурс, плюс его не перебил второй стажёр, и начальник (высокоприоритетная задача) не стоял в ожидании.
Ситуация. После того как стажёр кладёт письмо в ящик, он звонит начальнику (Коляну), чтобы он проверил ящик.
Семафор здесь — простой способ передать событие “готово письмо” из стажёра начальнику. Без семафора Вика бы бесконечно опрашивала ящик (polling), тратя энергию и время.
Ситуация. У вас есть только небольшой ящик, в который может поместиться не более трёх писем. Вы решили, что проверять ящик будет начальник только тогда, когда в нём накопится два и более писем. По факту это работа с обычным счетчиком.
Двоичный семафор [1] — это семафор, максимальный счетчик которого равен 1, отсюда и название «двоичный». Задача может «взять» семафор, только если он доступен, а семафор доступен, только если его счетчик равен 1.
в FreeRTOS двоичный семафор реализован как очередь длиной в 1 элемент, размером в 0 байт, где важен только факт — «есть разрешение» или «нет»
Он может быть только полон (после give) или пуст (после take), как очередь ёмкостью один.
Все, что вы делаете — либо give, либо take.
Данные не передаются (размер элемента = 0), передаётся лишь сам факт события: дали — значит событие произошло, взяли — значит событие обработано.
Функции API семафоров позволяют задать время блокировки. Время блокировки определяет максимальное количество тактов, на которые задача должна перейти в состояние «Блокировано» при попытке «занять» семафор, если семафор недоступен немедленно. Если на одном семафоре заблокировано несколько задач, то при следующем освобождении семафора разблокируется задача с наивысшим приоритетом.
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
— выделяет под семафор очередь ёмкостью в один «слот» (элемент размером 0).
Из задачи:
xSemaphoreGive(sem);
Из ISR:
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(sem, &woken);
portYIELD_FROM_ISR(woken);
Когда вы даёте семафор из прерывания, нужно учесть, что может быть разблокирована задача с более высоким приоритетом, и тогда стоит сразу же переключиться на неё. Вот что делают эти три строки:
BaseType_t woken = pdFALSE;
— заводим переменную-флаг woken, изначально равную pdFALSE. Она будет сигнализировать, разблокировал ли семафор задачу, у которой приоритет выше, чем у текущей.
xSemaphoreGiveFromISR(sem, &woken);
— даём семафор из ISR. Если в этот момент семафор «был взят» и теперь задача, ожидавшая его в xSemaphoreTake(), разблокируется, то в woken запишут pdTRUE. То есть ISR сообщает, что есть задача, которой надо «отдать» управление.
portYIELD_FROM_ISR(woken);
— смотрим флаг woken. Если он pdTRUE, макрос инициирует немедленный переключатель контекста после выхода из ISR, чтобы сразу запустить только что разблокированную, более приоритетную задачу. Если же woken == pdFALSE, переключения не происходит — текущая задача продолжит выполняться дальше.
if (xSemaphoreTake(sem, portMAX_DELAY) == pdTRUE) {
// семафор успешно взят
}
— блокирует текущую задачу до тех пор, пока семафор не станет «полным» (Give), после чего возвращает pdTRUE; при этом семафор снова переходит в пустое состояние и требует нового Give перед следующим Take.
Рассмотрим пример, когда задача используется для обслуживания периферийного устройства. Опрос периферийного устройства будет пустой тратой ресурсов и помешает выполнению других задач. Поэтому предпочтительно, чтобы задача проводила большую часть времени в заблокированном состоянии (позволяя другим задачам выполняться) и выполнялась только тогда, когда ей действительно нужно что-то сделать. Это достигается с помощью двоичного семафора: задача блокируется при попытке «захватить» семафор. Затем для периферийного устройства пишется процедура прерывания, которая просто «выдаёт» семафор, когда периферийному устройству требуется обслуживание.
Немного перепишем наше прерывание [2], которое мы написали пару уроков назад:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_attr.h"
#include "esp_log.h"
#define LED_GPIO GPIO_NUM_2 // Пин, к которому подключён светодиод
#define BUTTON_GPIO GPIO_NUM_23 // Пин, к которому подключена кнопка
TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink
static const char *TAG = "binary_sem";
// Бинарный семафор для синхронизации кнопка→задача
static SemaphoreHandle_t button_sem = NULL;
// Задача, которая постоянно устанавливает уровень на LED_GPIO согласно state
void Blink_Task(void *arg) {
while (1) {
if(xSemaphoreTake(button_sem, portMAX_DELAY)){
ESP_LOGI(TAG, "Button pressed!");
// Включаем LED на 500 мс
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(LED_GPIO, 0);
}
}
}
/*
Обработчик прерывания от кнопки.
Просто инвертирует переменную state.
IRAM_ATTR — атрибут, гарантирующий, что этот код попадёт в быстрый IRAM,
а не во флеш (важно для надёжности ISR).
*/
static void IRAM_ATTR gpio_isr_handler(void *arg) {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(button_sem, &woken);
portYIELD_FROM_ISR(woken);
}
void app_main() {
// Создаём бинарный семафор (он будет пустым)
button_sem = xSemaphoreCreateBinary();
if (button_sem == NULL) {
ESP_LOGE(TAG, "Failed to create semaphore");
return;
}
esp_rom_gpio_pad_select_gpio(LED_GPIO); // "Переключение" выбранного физического контакта в режим GPIO
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // Настройка LED_GPIO как цифровой вывод
gpio_set_level(LED_GPIO, 0); // сразу гасим
esp_rom_gpio_pad_select_gpio(BUTTON_GPIO); // "Переключение" выбранного физического контакта в режим GPIO
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT); // Настройка BUTTON_GPIO как входа
gpio_pullup_en(BUTTON_GPIO); // Подтяжка к VCC
gpio_set_intr_type(BUTTON_GPIO, GPIO_INTR_POSEDGE); // Прерывание при нажатии (положительный фронт)
gpio_install_isr_service(ESP_INTR_FLAG_IRAM); // Устанавливаем сервис ISR
gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, NULL); // Регистрируем функцию-обработчик прерывания
gpio_intr_enable(BUTTON_GPIO); // Включаем прерывания для кнопки
// Создаём задачу “мигания” по семафору
if (xTaskCreate(Blink_Task, "BLINK", 2048, NULL, 5, &Blink_Handle) != pdPASS) {
ESP_LOGE(TAG, "Failed to create Blink_Task");
}
}
// Бинарный семафор для синхронизации кнопка→задача
static SemaphoreHandle_t button_sem = NULL;
button_sem = xSemaphoreCreateBinary();
if (button_sem == NULL) {
ESP_LOGE(TAG, "Failed to create semaphore");
return;
}
// ISR: при спаде кнопки «даём» семафор
static void IRAM_ATTR button_isr(void *arg) {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(button_sem, &woken);
portYIELD_FROM_ISR(woken);
}
// Блокируемся, пока семафор не «дадут»
if (xSemaphoreTake(button_sem, portMAX_DELAY) == pdTRUE) {
ESP_LOGI(TAG, "Button pressed!");
// Включаем LED на 500 мс
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(LED_GPIO, 0);
}
Подобно тому, как двоичные семафоры можно рассматривать как очереди длиной один элемент, счётные семафоры можно рассматривать как очереди длиной больше одного элемента.
Счётный семафор расширяет идею бинарного: вместо одного флага он хранит счетчик от 0 до заданного максимума. Это полезно, когда нужно накопить несколько событий или разрешений и обработать их по одному.
Взятие (xSemaphoreTake) и отдача (xSemaphoreGive / xSemaphoreGiveFromISR) у двоичного и счётного семафоров абсолютно одинаковы по API — единственное отличие в том, как вы их создаёте.
// uxMaxCount = 5, uxInitialCount = 0
SemaphoreHandle_t sem = xSemaphoreCreateCounting(5, 0);
— при создании вы указываете максимальное (uxMaxCount) и начальное (uxInitialCount) значение счётчика:
Сделаем мини‑игру: если мы успеем нажать на кнопку более трёх раз за 5 секунд, то увидим три быстрых мигания, а если нет — одно.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_attr.h"
#include "esp_log.h"
#define LED_GPIO GPIO_NUM_2 // Пин, к которому подключён светодиод
#define BUTTON_GPIO GPIO_NUM_23 // Пин, к которому подключена кнопка
TaskHandle_t Blink_Handle = NULL; // Дескриптор задачи Blink
static const char *TAG = "counting_sem";
// Семафор
static SemaphoreHandle_t button_sem = NULL;
// Задача
void Blink_Task(void *arg) {
int count;
while (1) {
// Ждём 5 секунд
vTaskDelay(pdMS_TO_TICKS(5000));
// Считаем накопленные нажатия
count = 0;
while(xSemaphoreTake(button_sem, 0) == pdTRUE){
count++;
}
ESP_LOGI(TAG, "Button pressed %d times in last 5s", count);
if (count >= 3) {
// 3 быстрых моргания
for (int i = 0; i < 3; i++) {
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(200));
gpio_set_level(LED_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(200));
}
} else {
// Одно долгое
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(1000));
gpio_set_level(LED_GPIO, 0);
}
}
}
// ISR: при каждом нажатии ++счётчик
static void IRAM_ATTR gpio_isr_handler(void *arg) {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(button_sem, &woken);
portYIELD_FROM_ISR(woken);
}
void app_main() {
// Создаём счётный семафор (максимум 10, изначально 0)
button_sem = xSemaphoreCreateCounting(10, 0);
if (button_sem == NULL) {
ESP_LOGE(TAG, "Failed to create semaphore");
return;
}
esp_rom_gpio_pad_select_gpio(LED_GPIO); // "Переключение" выбранного физического контакта в режим GPIO
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT); // Настройка LED_GPIO как цифровой вывод
gpio_set_level(LED_GPIO, 0); // сразу гасим
esp_rom_gpio_pad_select_gpio(BUTTON_GPIO); // "Переключение" выбранного физического контакта в режим GPIO
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT); // Настройка BUTTON_GPIO как входа
gpio_pullup_en(BUTTON_GPIO); // Подтяжка к VCC
gpio_set_intr_type(BUTTON_GPIO, GPIO_INTR_POSEDGE); // Прерывание при нажатии (положительный фронт)
gpio_install_isr_service(ESP_INTR_FLAG_IRAM); // Устанавливаем сервис ISR
gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, NULL); // Регистрируем функцию-обработчик прерывания
gpio_intr_enable(BUTTON_GPIO); // Включаем прерывания для кнопки
// Создаём задачу “мигания” по семафору
if (xTaskCreate(Blink_Task, "BLINK", 2048, NULL, 5, &Blink_Handle) != pdPASS) {
ESP_LOGE(TAG, "Failed to create Blink_Task");
}
}
Двоичные семафоры и мьютексы очень похожи, но имеют некоторые тонкие различия: мьютексы включают механизм наследования приоритетов (priority inheritance), а двоичные семафоры — нет. Мьютексы отлично подходят для взаимного исключения.
Когда несколько задач разного приоритета борются за один и тот же ресурс (мьютекс), может возникнуть классическая проблема priority inversion:
Задача L (Low‑prio) берёт мьютекс и входит в критическую секцию.
Задача H (High‑prio) просыпается и пытается взять тот же мьютекс → блокируется, потому что L ещё не вернула мьютекс.
Задача M (Med‑prio) с приоритетом между L и H может встать на выполнение и «перекрыть» L, потому что H в очереди ждёт мьютекс и не может выполнить свою работу и вернуть управление.
В результате H простаивает, а L не получает CPU, чтобы освободить мьютекс — и весь алгоритм может «зависнуть» на задаче среднего приоритета, даже если H гораздо важнее.
FreeRTOS при захвате мьютекса автоматически сравнивает приоритеты владельца и ожидающей задачи:
Если владелец мьютекса (L) имеет приоритет ниже, чем ожидающая задача (H), то у L временно повышается приоритет до уровня H.
Это даёт L право продолжить выполняться даже в присутствии средних задач M, чтобы как можно скорее выйти из критической секции и вернуть мьютекс.
Как только L делает xSemaphoreGive(mutex), его приоритет возвращается к исходному значению, и планировщик снова сможет запустить H (или M, если H уже выполнена).
Спасибо, что дочитали до конца! Прошу прощения за долгое затишье — в ближайшее время выйдет новая статья, в которой мы продолжим изучать esp-idf. Буду рад вашим вопросам и пожеланиям в комментариях — пишите, о чём вам было бы интересно узнать в следующих материалах!
Автор: nikitatm333
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/esp/425687
Ссылки в тексте:
[1] Двоичный семафор: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/02-Binary-semaphores#:~:text=Updated%20Jul%202025-,FreeRTOS%20binary%20semaphores,-%5BSee%20also%20Blocking
[2] прерывание: https://habr.com/ru/articles/919362/#:~:text=%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20IRAM_ATTR.-,%D0%9F%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0,-%D0%9D%D0%B0%D1%88%D0%B0%20%D1%86%D0%B5%D0%BB%D1%8C%20%E2%80%94%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C
[3] Источник: https://habr.com/ru/articles/923128/?utm_campaign=923128&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.