В этой статье описано использование мьютексных семафоров (перевод статьи [1]), для простой в использовании и надежной реализации FreeRTOS для Arduino IDE (подробнее см. [2]). Все непонятные термины, касающиеся систем RTOS, см. в статье [5] (раздел "Словарик" в конце статьи).
Когда две или большее количество задач совместно используют какой-либо аппаратный ресурс, такой как последовательный порт Serial, то может возникнуть ситуация, когда планировщик RTOS выключит одну из задач до того, как она завершит свой последовательный вывод через Serial, и другая задача начнет свой вывод через через тот же Serial, в результате чего получится испорченный вывод у первой задачи.
Эту проблему можно решить с использованием семафора (который также называется флагом) для защиты аппаратного ресурса, чтобы его никто больше не мог использовать, пока семафор не будет освобожден той задачей, которая в настоящее время завладела аппаратным ресурсом.
"Семафор (англ. semaphore) — объект, ограничивающий количество потоков, которые могут войти в заданный участок кода. Определение введено Эдсгером Дейкстрой. Семафоры используются для синхронизации и защиты передачи данных через разделяемую память, а также для синхронизации работы процессов и потоков."
Также см. [3].
Мьютекстные семафоры это двоичные семафоры, в которых имеется механизм наследования приоритета (priority inheritance mechanism). Принимая во внимание, что двоичные семафоры - лучший выбор для реализации синхронизации (между задачами или между задачами и прерыванием), мьютексные семафоры лучший выбор для реализации простого взаимного исключения (MUTual EXclusion, отсюда произошел термин мьютекс). В этом примере мы будет использовать мьютексный семафор, чтобы защитить последовательный порт Serial.
Словечко мьютекс произошло от mutual exclusion, что означает "взаимное исключение". В контексте RTOS это некий программный объект, который дает исключительное право владения каким-то ресурсом тому потоку (задаче Task в терминах FreeRTOS), который завладел мьютексом. Остальные потоки, которым нужен тот же ресурс, вынуждены ожидать в блокировке.
Синхронизация в контексте RTOS - это ожидание одной задачей поступления данных от другой задачи. В качестве одной из задач может быть обработчик прерывания. Говорят, что задачи СИНХРОНИЗИРУЮТСЯ друг с другом, хотя это не очень очевидно звучит, если принять во внимание, что все это нужно для корректной передачи данных.
В жизни мы постоянно сталкиваемся с синхронизацией. Например, когда мы ждем поезд на вокзале, когда просыпаемся от звонка будильника, когда разговариваем с внимательным собеседником (когда оба тараторят, не слушая друг друга, то это уже не синхронизация).
Когда используется принцип взаимного исключения (mutual exclusion), мьютекс работает как токен, используемый для защиты ресурса. Когда задача (Task) хочет получить доступ к этому ресурсу, она должна сначала получить (или захватить, Take) токен. Когда задача завершила работу с ресурсом, она должна освободить (отдать, Give) токен обратно, позволяя другим задачам возможность получить доступ к тому же ресурсу.
Для начала экспериментов с семафором либо загрузите файл AnalogRead_DigitalRead.ino в среду Arduino IDE, либо сделайте copy и paste его кода в новый файл, который Вы должны будете впоследствии сохранить и назначить ему соответствующее имя.
Внутри скетча делается несколько шагов для создания и использования мьютексного семафора, чтобы защитить порт Serial.
Сначала для семафора нужно декларировать Handle в качестве глобальной переменной. Эта переменная должна быть глобальной, потому что к ней будут обращаться все задачи, которым понадобится семафор, когда они будут проверять семафор на возможность "взятия".
В функции setup() мы проверим, что что семафор еще не создан, путем проверки Handle на значение NULL. Далее мы создаем семафор, убеждаемся, что он корректно создан путем повторной проверки содержимого Handle (теперь там не должен быть NULL), и в завершении делаем семафор свободным ("отдаем" его), когда шедулер запустится.
Теперь всякий раз, когда задача хочет использовать функции Serial.println() порта Serial, она должна гарантировать, что перед этим взяла (Take) семафор, предназначенный для обозначения занятости порта Serial. Как только задача завершила работу с портом Serial, она должна отдать (Give) семафор, чтобы дать возможность другим задачам получить доступ к порту.
Теперь, когда Вы создали скетч с несколькими задачами, которые пишут в защищенный порт Serial, попробуйте, что произойдет, если они будут делать это без семафора, путем комментирования кода для "взятия" и "отдачи" семафора в каждой задаче.
Попробуйте печатать длинные строки текста в двух, трех или большем количестве задач, и смотрите что произойдет, когда задачи будут делать вывод строк с использованием защищающего семафора и без него.
Прочитайте подробнее про очереди (Queue), мьютексы (Mutex) и семафоры (Semaphore) в документации [3, 4]. Есть другие дополнительные типы семафоров, и важно понимать, в каком случае каждый тип будет наиболее эффективен.
#include <Arduino_FreeRTOS.h>// Добавление функций FreeRTOS для семафоров (или флагов, Flags):#include <semphr.h>// Декларирование хендла мьютексного семафора, который будет// использоваться для обслуживания порта Serial.// Хендл семафора нужен, чтобы гарантировать, что только одна// задача в любой момент времени получила доступ к Serial.
SemaphoreHandle_t xSerialSemaphore;
// Определение двух задач (Task) для цифрового (DigitalRead)// и аналогового (AnalogRead) ввода:voidTaskDigitalRead( void*pvParameters );
voidTaskAnalogRead( void*pvParameters );
// Функция setup запустится только один раз, когда Вы нажмете// кнопку сброса (reset), или когда на плату будет подано питание:voidsetup() {
// Инициализация последовательного обмена на скорости 9600 бит/сек:
Serial.begin(9600);
// Семафоры полезны для остановки задачи, когда она должна// быть поставлена на паузу для ожидания из-за необходимости// доступа к общему ресурсу. В нашем случае это порт Serial.// Семафоры должны использоваться только когда работает// планировщик, но здесь мы можем их настроить.// Проверка, что семафор уже не создан:if ( xSerialSemaphore ==NULL )
{
// Создание мьютексного семафора, который мы будем использовать// для обслуживания доступа к порту Serial:
xSerialSemaphore = xSemaphoreCreateMutex();
if ( ( xSerialSemaphore ) !=NULL )
{
// Если семафор создан, то делаем его доступным (Give)// для использования при доступе к порту Serial:
xSemaphoreGive( ( xSerialSemaphore ) );
}
}
// Теперь настроим 2 задачи, чтобы они работали независимо.
xTaskCreate(
TaskDigitalRead
, (const portCHAR *)"DigitalRead"// Это просто любое читаемое имя
, 128// этот размер стека задачи может быть проверен и подстроен// с помощью чтения значения Stack Highwater
, NULL
, 2// приоритет (1 самый высший, 4 самый низший) задачи
, NULL );
xTaskCreate(
TaskAnalogRead
, (const portCHAR *) "AnalogRead"
, 128// размер стека задачи
, NULL
, 1// приоритет задачи
, NULL );
// Теперь автоматически запустится планировщик задач (Task scheduler),// который будет управлять их запуском.
}
voidloop()
{
// Здесь пустота. Все действия выполняются в теле задач (Tasks).
}
/*--------------------------------------------------*//*----------- Tasks (определения задач) ------------*//*--------------------------------------------------*/voidTaskDigitalRead( void*pvParameters __attribute__((unused)) )
{
/* DigitalReadSerial Эта задача читает цифровой вход 2, и печатает результат в консоль порта Serial. */// К цифровому выводу 2 подключена кнопка. Дадим ему имя:uint8_t pushButton =2;
// Настроим этот вывод как вход:
pinMode(pushButton, INPUT);
for (;;) // Бесконечный цикл задачи.
{
// Чтение уровня на цифровом входе:int buttonState = digitalRead(pushButton);
// Посмотрим, можем ли мы "взять" (Take) семафор.// Если семафор недоступен, ждем 5 тиков планировщика,// после чего снова проверим семафор.if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
{
// Здесь мы можем "взять" семафор, и таким образом получить// исключительный доступ к общему ресурсу Serial.// Исключительный доступ к порту Serial нужен для того, чтобы// наш выводимый текст не было испорчен другими задачами.// Напечатаем состояние кнопки:
Serial.println(buttonState);
// Теперь освободим семафор (или "отдадим" его, Give), чтобы// порт Serial был доступен для любых задач.
xSemaphoreGive( xSerialSemaphore );
}
// Задержка в 1 тик (15 мс) между чтениями, для стабильности:
vTaskDelay(1);
}
}
voidTaskAnalogRead( void*pvParameters __attribute__((unused)) )
{
for (;;)
{
// Прочитаем значение аналогового входа 0:int sensorValue = analogRead(A0);
// Посмотрим, можем ли мы "взять" (Take) семафор.// Если семафор недоступен, ждем 5 тиков планировщика,// после чего снова проверим семафор.if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
{
// Здесь мы можем "взять" семафор, и таким образом получить// исключительный доступ к общему ресурсу Serial.// Исключительный доступ к порту Serial нужен для того, чтобы// наш выводимый текст не было испорчен другими задачами.// Напечатаем прочитанное значение:
Serial.println(sensorValue);
// Теперь освободим семафор (или "отдадим" его, Give), чтобы// порт Serial был доступен для любых задач.
xSemaphoreGive( xSerialSemaphore );
}
// Задержка в 1 тик (15 мс) между чтениями, для стабильности:
vTaskDelay(1);
}
}