Как мы уже знаем, работать с ESP32 нам приходится с использованием операционной системы реального времени FreeRTOS. Это вызвано многими причинами. Во-первых, использование операционной системы обусловлено тем, что контроллер ESP32 имеет на своём борту модуль для работы с беспроводными соединениями. А это сеть и работа с сетевыми протоколами, как мы уже давно знаем, непростая и сервить обмен по сети без использования систем реального времени, очень тяжело и, следовательно, велик процент ошибок. Во-вторых, контроллер ESP32 двухъядерный. Это также предусматривает использование системы реального времени.
Насчёт теории по работе с FreeRTOS. Мы уже достаточно неплохо знаем данную систему из опыта по программированию контроллеров STM32 и ESP8266. Также мы уже написали достаточно кода и на ESP32 с использованием данной операционной системы. Поэтому вдаваться в азы работы с FreeRTOS не имеет смысла во избежание потери лишнего времени. Поэтому уроки по FreeRTOS будут носить более закрепительный характер. Мы будем знакомиться с таким материалом, с которым мы ещё не встречались. Также будем работать и с знакомыми разделами, если по работе с ними будет много нового.
Напомню лишь то, что FreeRTOS — многозадачная операционная система реального времени (ОСРВ) для встраиваемых систем.
И сегодня на повестке дня у нас мьютексы.
Слово мьютекс происходит от английского словосочетания mutual exclusion — взаимное исключение.
Мьютекс — это примитив синхронизации, обеспечивающий взаимное исключение исполнения критических участков кода.
Это приблизительно то же самое, что и двоичный семафор, только принцип работы с мьютексом немного отличается.
С двоичными семафорами мы очень плотно знакомились в уроке 104 по контроллеру STM32. Я очень много там рассказывал о смысле критических секций в коде.
Напомню лишь, что очень нередко случается ситуация, когда существует какой-нибудь ответственный процесс, который в данный момент времени должен вызываться только из одной задачи, то есть не должно быть одновременного выполнения данного процесса несколькими задачами. Например, может быть вывод в какой-то порт информации процессом, если данная задача в это же время будет вызвана процессом другим, то скорей всего данные в лучшем случае перемешаются, а в худшем половина их растеряется вообще.
В чём же всё-таки отличие мьютекса от двоичного семафора? Отличие, впрочем, здесь невелико. Во-первых, семафоры больше используются в механизме сигнализации из одной задачи в другую о каком-то событии, мьютекс — это более механизм блокировки. Во-вторых, мьютекс — это объект, а семафор — целое число. Есть ещё различия, но они незначительны.
Думаю, понять и усвоить, что же такое мьютекс и каков его смысл нам поможет практическая работа с ним.
Схема урока у нас не изменилась с прошлого занятия, так как дисплей нам опять потребуется
Проект был сделан также на основе проекта прошлого урока с именем SOFT_TIMER и назван был MUTEX_LCD.
Откроем наш проект в Espressif IDE.
Мы не будем сегодня использовать очереди в выводе строк на дисплей, а будем выводить текст в дисплей напрямую, так как очереди не дадут нам увидеть смысл мьютексов. Поэтому удалим объявление структуры и переменную её типа
typedef struct
{
unsigned char y_pos;
unsigned char x_pos;
char *str;
} qLCDData;
//————————————————
xQueueHandle lcd_string_queue = NULL;
Не нужна нам, соответственно, будет и функция vLCDTask, поэтому удалим её вместе с телом.
Функции по работе с таймерами periodic_timer_callback и oneshot_timer_callback также удалим вместе с их телами.
Создадим функции для четырёх аналогичных задач, в которых заведём счётчики и результат счёта будем выводить на дисплей в позиции, различные для каждой задачи. Также период работы счётчика засчёт разных задержек будет различным у каждой задачи
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
//------------------------------------------------ static void task1(void* arg) { uint16_t cnt = 0; char str1[10] = {0}; for(;;) { sprintf(str1,"%05d",cnt); LCD_SetPos(0,0); LCD_String(str1); vTaskDelay(1000 / portTICK_PERIOD_MS); cnt++; } } //------------------------------------------------ static void task2(void* arg) { uint16_t cnt = 0; char str1[10] = {0}; for(;;) { sprintf(str1,"%05d",cnt); LCD_SetPos(0,1); LCD_String(str1); vTaskDelay(900 / portTICK_PERIOD_MS); cnt++; } } //------------------------------------------------ static void task3(void* arg) { uint16_t cnt = 0; char str1[10] = {0}; for(;;) { sprintf(str1,"%05d",cnt); LCD_SetPos(0,2); LCD_String(str1); vTaskDelay(800 / portTICK_PERIOD_MS); cnt++; } } //------------------------------------------------ static void task4(void* arg) { uint16_t cnt = 0; char str1[10] = {0}; for(;;) { sprintf(str1,"%05d",cnt); LCD_SetPos(0,3); LCD_String(str1); vTaskDelay(700 / portTICK_PERIOD_MS); cnt++; } } //------------------------------------------------ |
В функции app_main удалим создание очереди и задачи для дисплея
lcd_string_queue = xQueueCreate(10, sizeof(qLCDData));
xTaskCreate(vLCDTask, «vLCDTask», 2048, NULL, 2, NULL);
После строки
LCD_ini();
удалим весь код до бесконечного цикла.
Теперь наш проект соберётся.
Создадим наши задачи с небольшой задержкой между созданием
1 2 3 4 5 6 7 8 9 |
LCD_ini(); vTaskDelay(100 / portTICK_PERIOD_MS); xTaskCreate(task1, "task1", 2048, NULL, 5, NULL); vTaskDelay(100 / portTICK_PERIOD_MS); xTaskCreate(task2, "task2", 2048, NULL, 5, NULL); vTaskDelay(100 / portTICK_PERIOD_MS); xTaskCreate(task3, "task3", 2048, NULL, 5, NULL); vTaskDelay(100 / portTICK_PERIOD_MS); xTaskCreate(task4, "task4", 2048, NULL, 5, NULL); |
Соберём код, прошьём контроллер. Я думаю, многие уже догадались, что у нас будет твориться на дисплее
Такой венигрет происходит из-за того, что мы бесконтрольно отправляем данные в дисплей по шине I2C и контроллер дисплея даже не может успеть понять, что ему пришло. В данном случае отправка данных из задачи в I2C и есть наша критическая секция, которая должна выполняться полностью и непрерывно только одним потоком.
Поэтому как-то надо наши данные отправлять в порядке очереди. В этом нам и поможет мьютекс, который мы сейчас создадим. Обычно в данном случае объявляется глобальная переменная. Но мы пойдём другим путём. Хотя в использовании глобальной переменной в нашем случае нет ничего страшного, так как мы не будем менять свойства мьютекса в процессе работы кода, а будем их только читать, но тем не менее считается более правильным передача в данном случае указателя на мьютекс через параметры задачи.
Поэтому объявим и создадим мьютекс в app_main
1 2 |
LCD_ini(); SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); |
Структура для свойств мьютекса та же самая, что и для семафора. Отличается только функция для его создания.
Теперь в параметре при создании каждой задачи мы передадим указатель на наш мьютекс
xTaskCreate(task1, "task1", 2048, mutex, 5, NULL);
...
xTaskCreate(task2, "task2", 2048, mutex, 5, NULL);
...
xTaskCreate(task3, "task3", 2048, mutex, 5, NULL);
...
xTaskCreate(task4, "task4", 2048, mutex, 5, NULL);
Также в функциях для каждой задачи (task1, task2, task3 и task4) мы извлечём указатель на мьютекс из параметров
1 2 3 |
static void task1(void* arg) { SemaphoreHandle_t mutex = (SemaphoreHandle_t) arg; |
1 2 3 |
static void task2(void* arg) { SemaphoreHandle_t mutex = (SemaphoreHandle_t) arg; |
1 2 3 |
static void task3(void* arg) { SemaphoreHandle_t mutex = (SemaphoreHandle_t) arg; |
1 2 3 |
static void task4(void* arg) { SemaphoreHandle_t mutex = (SemaphoreHandle_t) arg; |
Ясное дело, что мы могли не создавать функцию для каждой задачи, а пользоваться одной. Задержку можно было передать тоже в параметрах, создав структуру из значения задержки, номера строки для дисплея и указателя на мьютекс. Причём, так и следует делать. Только, отдельные функции придают более читабельный вид коду.
Далее я не буду выкладывать код для функций всех задач, потому что он будет абсолютно одинаковым. Поэтому во всех задачах в бесконечном цикле перед выводом строки добавим следующую строку с кодом
1 2 |
sprintf(str1,"%05d",cnt); xSemaphoreTake(mutex, portMAX_DELAY); |
Это будет блокировка входа для других задач, так как используем мы в других задачах один и тот же мьютекс, мы же передавали указатель на один мьютекс. Имя функции такое же как для команды взять семафор.
После того, как данные отправятся в шину I2C, мы разблокируем наш мьютекс, чтобы другие задачи могли также работать с дисплеем
1 2 |
LCD_String(str1); xSemaphoreGive(mutex); |
То есть, отправка данных в шину или вот этот код
LCD_String(str1);
xSemaphoreGive(mutex);
и есть критическая секция, которую должен выполнять непрерывно только один поток.
Проверим теперь работу нашего кода
Вот теперь всё работает правильно. У нас создаётся впечатление, что каждая строка живёт самостоятельной жизнью. В этом и есть задача операционных систем в плане многозадачности.
Итак, на данном уроке мы познакомились с мьютексами, механизмом их использования и с тем, как они нам помогают организовать критические секции, а также их защитить от одновременного использования потоками, в нашем коде.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь Недорогие отладочные платы ESP32
Недорогие отладочные платы ESP32/ESP32-C3/ESP32-S3 можно купить здесь Недорогие отладочные платы ESP32
Переходник I2C to LCD можно приобрести здесь I2C to LCD1602 2004
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Добавить комментарий