Использование семафоров и мьютексов в FreeRTOS на Arduino Uno

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

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

Использование семафоров и мьютексов в FreeRTOS на Arduino Uno

Что такое семафор (Semaphore)

В статье по основам FreeRTOS в Arduino мы изучили, что у всех задач есть их приоритеты и когда задача более высокого приоритета прерывает выполнение задачи более низкого приоритета, то это может привести к потере/повреждению данных задачи более низкого приоритета поскольку задача не будет выполняться, а данные для нее в это время могут продолжать поступать (например, от какого-нибудь датчика). В результате мы получим неудовлетворительную работу всего приложения.

Для защиты ресурсов от подобных потерь важную роль играют семафоры.

Семафор представляет собой сигнальный механизм, в котором задача, находящаяся в режиме ожидания (waiting state), сигналит об этом другой задаче. К примеру, если задача task1 заканчивает свою работу, она передает флаг или инкремент флага на 1 и когда этот флаг принимается другой задачей (task2), это говорит ей о том, что теперь она может выполнять свою работу. Когда задача task2 заканчивает свою работу значение флага уменьшается на 1.

Таким образом, мы имеем дело с механизмом "передать" и "принять" (“Give” and “Take” mechanism), а семафор является переменной целого типа, которая используется для синхронизации доступа к ресурсам.

В FreeRTOS возможны семафоры двух типов:

  1. Бинарные семафоры (Binary Semaphore).
  2. Счетные семафоры (Counting Semaphore).

1. Бинарные семафоры – могут принимать значения 0 и 1. В некотором смысле такой семафор похож на очередь длины 1. К примеру, у нас есть две задачи - task1 и task2. Task1 передает данные задаче task2, при этом задача task2 непрерывно проверяет равен ли элемент очереди 1. Если он равен 1, то task2 может считывать данные, иначе должна подождать до тех пор пока он не равен 1. После получения данных task2 декрементирует (уменьшает на 1) элемент очереди, таким образом, он становится равным 0. Это будет означать что задача task1 снова может передавать данные задаче task2.

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

2. Счетный семафор – может принимать не только значения равные 0 и 1, поэтому он эквивалентен очереди с длиной больше 1 элемента. Этот вид семафоров используется для событий счета. В данном случае обработчик события будет "передавать" семафор каждый раз когда происходит событие (инкрементирование значения счета семафора), а обработчик задачи будет "принимать" семафор каждый раз когда он обрабатывает событие (декрементирование значения счета семафора).

В данном случае значение счета, следовательно, будет равно разнице между числом событий, которые случились (произошли), и числом событий, которые были обработаны.

Кратко теорию по семафорам мы рассмотрели, теперь посмотрим каким образом их можно использовать в FreeRTOS.

Как использовать семафоры в FreeRTOS

FreeRTOS поддерживает различные API (application programming interface - программный интерфейс приложения) функции для создания семафоров, их передачи и приема.

Можно использовать два типа API функций для одного и того же объекта ядра. Если нам нужно передать семафор из подпрограммы обработки прерывания (ISR), то в этом случае нельзя использовать API для обычного семафора. В данном случае необходимо использовать защищенное от прерываний API.

В данной статье мы рассмотрим использование только бинарных семафоров поскольку они проще для понимания. Поскольку в данном проекте мы будем использовать прерывания, то нам необходимо будет использовать защищенное от прерываний API в функции обработки прерывания (ISR function). При этом когда мы будем говорить о синхронизации между задачей и прерыванием, это будет означать перевод задачи в работающее состояние (Running state) после выполнения функции обработки прерывания (ISR function).

Создание семафора

Чтобы использовать любой объект ядра его сначала необходимо создать. Для создания бинарного семафора мы будем использовать функцию vSemaphoreCreateBinary().

Эта API функция не требует никаких параметров и возвращает переменную типа SemaphoreHandle_t. Для хранения значения семафора мы создадим глобальную переменную sema_v.

Передача семафора

Для передачи семафора есть два способа – один для прерывания, а другой – для нормальной задачи:

  1. xSemaphoreGive() - этой API функции нужен всего один аргумент, это имя переменной семафора (в нашем случае, к примеру, sema_v). Эту функцию можно вызвать из любой обычной задачи которую вы хотите синхронизировать.
  2. xSemaphoreGiveFromISR() – это защищенная от прерываний версия функции xSemaphoreGive(). Когда нам необходимо синхронизировать функцию обработки прерывания (ISR) и обычную задачу в этом случае следует вызывать функцию xSemaphoreGiveFromISR() из функции обработки прерывания (ISR function).

Прием семафора

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

xSemaphore: имя семафора, в нашем случае это sema_v.

xTicksToWait: максимальное время, которое задача будет ждать в состоянии блокировки (Blocked state) до тех пор, пока семафор не станет доступным. В нашем проекте мы установим параметр xTicksToWait в значение portMAX_DELAY чтобы задача task_1 могла бесконечно долго ждать в состоянии блокировки до тех пор пока семафор sema_v не станет доступным.

Теперь рассмотрим использование этих функций на конкретных примерах.

В нашем проекте мы подключим к плате Arduino Uno одну кнопку и два светодиода. Кнопка будет играть роль активатора прерывания и она будет подсоединена к контакту 2 платы Arduino Uno. При нажатии этой кнопки будет генерироваться сигнал прерывания и зажигаться светодиод, подключенный к контакту 8 платы Arduino. При повторном нажатии кнопки светодиод будет выключаться.

Таким образом, при нажатии кнопки функция xSemaphoreGiveFromISR() будет вызываться из функции обработки прерывания (ISR function), а функция xSemaphoreTake() будет вызываться из функции TaskLED.

Чтобы придать нашему проекту некоторую многозадачность, мы подсоединим второй светодиод к контакту 7 платы Arduino Uno, который всегда будет в мигающем состоянии.

Объяснение кода программы для использования семафора

Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.

1. Первым делом в программе подключим заголовочный файл Arduino_FreeRTOS.h. Также, если мы используем в программе любой объект ядра, например, семафор, то нам необходимо будет подключить заголовочный файл библиотеки и для него. В нашем случае мы подключаем библиотеку semphr.h.

2. Объявим переменную типа SemaphoreHandle_t чтобы сохранять в ней значение семафора.

3. В функции void setup() создадим две задачи (TaskLED и TaskBlink) используя API функцию xTaskCreate(). Затем мы создадим семафор с помощью функции xSemaphoreCreateBinary(). Мы создадим задачи с одинаковыми приоритетами, но вы в дальнейшем можете самостоятельно поэкспериментировать со значениями приоритетов. Также в функции void setup() мы зададим режим работы для контакта 2 на ввод данных с внутренним подтягивающим резистором и сделаем его контактом для обработки прерывания. Более подробно об использовании прерываний в платах Arduino вы можете прочитать в этой статье. И, наконец, запустим планировщик (scheduler).

4. Далее запрограммируем функцию для обработки прерывания (ISR function). Ее следует назвать точно так, как второй аргумент функции для создания прерывания attachInterrupt(). Чтобы прерывания в нашем проекте работали корректно, необходимо устранить эффект дребезга контактов у кнопки (debounce problem). Для этого в созданной функции мы будем использовать функции millis и micros для определения времени, большего чем время, в течение которого проявляется эффект дребезга контактов (debouncing time). Также из созданной функции мы будем вызывать функцию interruptHandler() как показано в следующем участке кода.

В функции interruptHandler() мы будем вызывать API функцию xSemaphoreGiveFromISR().

Эта функция будет передавать семафор в функцию TaskLed для включения/выключения светодиода.

5. Создадим функцию TaskLed и в ней внутри цикла while мы будем вызывать API функцию xSemaphoreTake() и проверять был ли семафор успешно принят или нет. Если он равен pdPASS (то есть 1), то мы будем переключать состояние светодиода.

6. Также создадим функцию для мигания светодиода, подключенного к контакту 7 платы.

7. Функция void loop в программе будет оставаться пустой – не забывайте об этом.

Теперь загрузите код программы в плату Arduino и подсоедините к ней кнопку и светодиоды как показано на ниже приведенной схеме.

Схема проекта

Схема проекта для демонстрации использования семафоров в FreeRTOS в Arduino представлена на следующем рисунке.

Схема проекта для демонстрации использования семафоров в FreeRTOS в ArduinoПосле загрузки кода программы в плату Arduino вы увидите как один светодиод будет мигать каждые 200 мс, а при нажатии кнопки второй светодиод будет немедленно зажигаться. Более подробно эти процессы вы можете посмотреть на видео, приведенном в тексте статьи.

Демонстрация работы семафоров в нашем проекте

Таким образом, мы рассмотрели каким образом можно использовать семафоры в FreeRTOS в Arduino чтобы передавать данные от одной задачи к другой без потерь.

Что такое мьютексы (Mutex)

Как было рассмотрено в первой части статьи, семафор представляет собой механизм сигнализации, аналогично этому, мьютекс является механизмом блокирования. Но семафор использует отдельные функции для инкрементирования и декрементирования (то есть передачи и приема), а в мьютексе все это совмещено в одной функции. Фактически, мьютекс представляет собой способ предотвращения повреждения общих ("расшаренных", shared) ресурсов.

Чтобы предотвратить повреждение общих ресурсов необходимо каждому ресурсу назначить свой номер (токен) карты (мьютекс). Любой, кто владеет этой картой, может получить доступ к ресурсу. Остальные в это время должны ждать до тех пор пока карта не возвратится (освободится). Таким образом, только одна задача может получить доступ к ресурсу, а остальные должны ждать своего шанса.

Рассмотрим использование мьютексов в FreeRTOS на примере.

Допустим, у нас есть три задачи. Первая – вывод данных на экран ЖК дисплея, вторая – передача данных от фоторезистора задаче ЖК дисплея, третья – передача данных температуры на ЖК дисплей. Получается, что у нас две задачи делят общий ресурс ЖК дисплея. Если задача фоторезистора и задача температуры будут передавать данные одновременно, то одни их этих данных могут быть повреждены или потеряны.

Таким образом, чтобы защитить данные от потери, нам нужно заблокировать ресурс ЖК дисплея для задачи task1 до тех пока не закончится выполнение задачи ЖК дисплея. После этого задача ЖК дисплея будет разблокирована и задача task2 сможет выполнить свою работу.

Подробно работа мьютексов и семафоров представлена на следующей диаграмме. Она на английском языке, но я надеюсь, ее все равно понять будет вам несложно.

Принцип работы мьютексов и семафоров на диаграмме

Как использовать мьютексы в FreeRTOS

Мьютексы используются примерно также, как и семафоры. Сначала его нужно создать, затем передать и принять с помощью соответствующих API функций.

Создание мьютекса

Для создания мьютекса можно использовать API функцию xSemaphoreCreateMutex(). Как следует из ее названия, мьютекс представляет собой тип бинарного семафора. Но мьютекс и семафор используются для различных целей: бинарный семафор – для синхронизации между задачами, а мьютекс – для защиты от повреждения/потери общих ресурсов.

Данная API функция не содержит никаких аргументов и возвращает переменную типа SemaphoreHandle_t. Если мьютекс не может быть создан, то функция xSemaphoreCreateMutex() вернет значение NULL.

Прием мьютекса

Когда задача хочет получить доступ к доступ она будет принимать мьютекс с помощью API функции xSemaphoreTake(). Фактически, это тоже самое что и прием бинарного семафора. В этой функции есть два параметра.

xSemaphore: имя мьютекса, который будет приниматься, в нашем случае mutex_v.

xTicksToWait: максимальное время, которое задача будет ждать в состоянии блокировки (Blocked state) до тех пор, пока мьютекс не станет доступным. В нашем проекте мы установим параметр xTicksToWait в значение portMAX_DELAY чтобы задача task_1 могла бесконечно долго ждать в состоянии блокировки до тех пор пока мьютекс mutex_v не станет доступным.

Передача мьютекса

После успешного доступа к общему ресурсу (shared resource) задача должна вернуть мьютекс чтобы другие задачи могли получить к нему доступ. Для возврата мьютекса обратно может быть использована API функция xSemaphoreGive().

Функция xSemaphoreGive() содержит только один аргумент – это мьютекс, который необходимо передать, в нашем случае это mutex_v.

Рассмотрим пример использования описанных функций в коде FreeRTOS используя Arduino IDE.

Объяснение кода программы для использования мьютекса

Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.

В нашей программе мы будем использовать монитор последовательного порта (Serial monitor) в качестве общего (разделяемого) ресурса и две задачи, которые будут стараться получить доступ к этому монитору для печати в нем определенных сообщений.

1. Заголовочные файлы будут такими же, как и в рассмотренном выше примере с семафором.

2. Объявим переменную типа SemaphoreHandle_t чтобы хранить в ней значения мьютекса.

3. В функции void setup() инициализируем последовательный порт для связи со скоростью 9600 бод и создадим две задачи (Task1 и Task2) используя API функцию xTaskCreate(). Затем создадим мьютекс с помощью функции xSemaphoreCreateMutex(). Мы создадим задачи с одинаковыми приоритетами – в дальнейшем вы можете самостоятельно поэкспериментировать с их изменениями.

4. Далее запрограммируем функции для выполнения задач Task1 и Task2. В цикле while этих функций перед печатью сообщения в монитор последовательного порта мы должны принять мьютекс с помощью функции xSemaphoreTake(), затем напечатать сообщение и затем вернуть (передать) мьютекс используя функцию xSemaphoreGive(). После этого сделаем в программе небольшую задержку.

Аналогичным образом запрограммируем и функцию для задачи Task2 с задержкой в ней на 500 мс.

5. Void loop() будет оставаться пустой.

Теперь можете загрузить код программы в плату Arduino и откройте окно монитора последовательной связи.

Вы должны увидеть в нем печатаемые сообщения от задач task1 и task2.

Печатаемые в окне монитора последовательной связи сообщения от задач task1 и task2

Чтобы протестировать работу мьютекса просто закомментируйте функцию xSemaphoreGive(mutex_v) в любой из задач. После этого вы увидите как программа зависнет на последнем напечатанном сообщении.

Зависание программы после комментирования одной из функций передачи мьютекса

Таким образом, в данной статье мы рассмотрели использование семафоров и мьютексов в операционной системе FreeRTOS на плате Arduino. Для получения более подробной информации по данным вопросам вы можете обратиться к официальной документации по FreeRTOS.

Исходный код программы (скетча)

Программа для использования семафора

Программа для использования мьютекса

Видео, демонстрирующее работу проекта

(Проголосуй первым!)
Загрузка...
983 просмотров

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

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