Взаимодействие потоков
Стабильность работы системы напрямую зависит от взаимодействия задач. FreeRTOS предлагает ряд механизмов для защиты памяти, кода и аппаратных ресурсов. Рассмотрим API-функции, позволяющие использовать эти механизмы.
Критические секции
Для создания критической секции могут применяться два подхода.
Первый неявным образом мы уже рассматривали, когда говорили про реализацию кучи из heap_3.c
. Приведем код функции vPortFree()
из этого файла:
void vPortFree( void *pv )
{
if( pv ) {
vTaskSuspendAll();
{
free( pv );
}
xTaskResumeAll();
}
}
Перед вызовом функции free()
все задачи приостанавливаются, т.е. xTaskSuspendAll()
отключает планировщик. По окончании данного участка планировщик возобновляет свою работу, xTaskResumeAll()
. Такой способ, тем не менее, не защищает от прерываний.
Другой способ организовать критическую секцию защитит код как от других задач, так и от прерываний, приоритет которых ниже константы configMAX_SYSCALL_INTERRUPT_PRIORITY
из файла конфигурации.
taskENTER_CRITICAL();
{
// critical section
}
taskEXIT_CRITICAL();
Очереди
Мы уже рассмотрели механизм работы очереди в предыдущей главе, перейдем к API ОСРВ. Для создания очереди используется функция (макрос) xQueueCreate()
(osMessageCreate()
).
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
Первый аргумент отвечает за длину создаваемой очереди, а второй за размер типа данных. Если очередь успешно создана, то функция возвращает ее идентификатор, который потребуется, когда задача захочет что-то записать в нее. Если в куче не окажется достаточно места, то вместо идентификатора функция вернет NULL
.
Пример создания очереди (xQueueHandler
— это синоним QueueHandle_t
):
xQueueHandle q = xQueueCreate(5, sizeof(uint32_t));
Классическая очередь подразумевает, что запись осуществляется с конца. Используйте функцию xQueueSendToBack()
либо ее синоним xQueueSend()
(osMessagePut()
)from_isr. Однако существует и специальная функция xQueueSendToFront()
для записи данных в начало очереди. Ее прототип представлен ниже.[from_isr] Для операций чтения и записи из прерывания необходимо использовать предназначенные для этого функции. К названию прибавляется суфикс FromISR
, например xQueueSendFromISR()
.
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);
Эта функция имеет три параметра: xQueue
— идентификатор; pvItemToQueue
— указатель на элемент (размер определен на этапе создания очереди); xTicksToWait
— максимальное количество квантов времени, которое задача может пребывать в режиме ожидания, пока не появится свободное место для записиrate_ms. При этом если в файле конфигурации INCLUDE_vTaskSuspend
равен 1
, то установка xTicksToWait
равным portMAX_DELAY
приведет к тому, что тайм-аут перестанет работать, и задача будет ожидать возможности записать данные бесконечно долго.[rate_ms] Для представления времени в миллисекундах следует желаемое число миллисекунд разделить на макроопределение portTICKS_RATE_MS
.
xQueueSend(q, (void *) &data, 100);
Для считывания существует две функции. Одна из них приводит к считыванию с последующим удалением — xQueueRecieve()
(osMessageGet()
), а вторая, наоборот, после чтения оставляет ячейку нетронутой — xQueuePeek()
(osMessagePeek()
).
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);
Для получения количества записанных элементов можно воспользоваться функцией uxQueueMessagesWaiting()
(osMessageWaiting()
). Для удаления очереди применяется функция vQueueDelete()
(osMessageDelete()
).
За более детальным описанием всех функций очереди следует обратиться к официальной документации.
Семафоры и мьютексы
Во FreeRTOS и семафоры и мьютексы имеют одинаковую природу, поэтому для работы с ними используются одни и те же функции.
SemaphoreHandle_t xSemaphoreCreateBinary( void );
SemaphoreHandle_t xSemaphoreCreateCounting(const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount);
SemaphoreHandle_t xSemaphoreCreateMutex( void );
Для захвата и отпускания любого типа семафора используются функции:
BaseType_t xSemaphoreGive(QueueHandle_t xQueue, TickType_t xTicksToWait);
BaseType_t xSemaphoreGiveFromISR(QueueHandle_t xQueue, BaseType_t * const pxHigherPriorityTaskWoken);
BaseType_t xSemaphoreTake(QueueHandle_t xQueue, TickType_t xTicksToWait);
BaseType_t xSemaphoreTakeFromISR(QueueHandle_t xQueue, BaseType_t * const pxHigherPriorityTaskWoken);
Аргумент xTicksToWait
позволяет задать максимальное количество квантов времени, в течение которого задача может находиться в блокированном состоянии при условии, что семафор невозможно захватить. Задав xTickToWait
равным константе portMAX_DELAY
, вы позволите задаче находиться в блокированном состоянии бесконечно долгоinclude_suspend.[include_suspend] Параметр INCLUDE_vTaskSuspend
в конфигурационном файле должен быть выставлен в 1
.
Приведем пример использования бинарного семафора.
SemaphoreHandle_t xSemaphore;
void vATask( void * arguments ) {
xSemaphore = xSemaphoreCreateBinary();
if( xSemaphore == NULL ) { // semaphore wasn't created
} else { // semaphore was created
}
}
За более детальным описанием всех функций семафоров следует обратиться к официальной документации.
Уведомления задач
Кроме мьютексов и семафоров FreeRTOS предоставляет еще один механизм для взаимодействия — уведомления (англ. notification). Они позволяют разблокировать задачу во время выполнения другой задачи или прерывания. Можно сказать, что задача А уведомляет задачу Б о том, что ей нужно выполниться. При этом необходимость создавать объект коммуникации отпадает — задачи общаются напрямую. В общем случае такой способ позволит ускорить выполнение до 45% и потребует меньше оперативной памяти, однако у уведомлений есть свои ограничения.
- Уведомлять можно только задачи, передача уведомлений в прерывание невозможна.
- Уведомление отправляется непосредственно в задачу, поэтому оно будет обрабатываться только ей.
- Буферизация не поддерживается. Следовательно, обработать ситуацию, когда задача, получив уведомление и перейдя в состояние готовности, получает еще одно, невозможно.
Этот механизм является опциональным, т.е. для его включения необходимо включить поддержку данной функциональности в файле конфигурации. В случае использования в структуру tskTCB
добавляется два поля.
// tskTCB
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif
Обращаться к ним напрямую не получится, так как они часть приватной (область видимости — модуль) структуры файла task.c
, но на них можно влиять, вызывая специализированные API-функции.
Для отправки уведомления используются xTaskNotify()
и xTaskNotifyGive()
, а также их версии для обработчиков прерываний. Принимающая задача должна выполнить xTaskNotifyWait()
или ulTaskNotifyTake()
. Каждая из функций имеет свои параметры, описанные в документации, и подходит для определенных случаев. Мы не будем рассматривать все, а лишь приведем пример, где семафор заменяется на уведомления для выполнения той же задачи — синхронизации задачи и прерывания.
const TickType_t event_period = pdMS_TO_TICKS( 500UL );
static void vHandlerTask( void *pvParameters ) {
// max_expected_time should be larger than interrupt period
const TickType_t max_expected_time = event_period + pdMS_TO_TICKS(5);
uint32_t value;
while(1) {
value = ulTaskNotifyTake( pdTRUE, max_expected_time );
if( value != 0 ) {
// do something here
} else {
// interrupt occurred after max_expected_time
// you should handle this situation
}
}
}
Напишем обработчик прерывания.
void EXTIA_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// sending a notification
vTaskNotifyGiveFromISR(tskHandler, &xHigherPriorityTaskWoken);
// if xHigherPriorityTaskWoken is pdTRUE in vTaskNotifyGiveFromISR()
// function return then the next function will call context switch
// routine, otherwise will do nothing.
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
Программные таймеры
В FreeRTOS начиная с 7-й версии ввели такую сущность, как программные таймеры (англ. timer). Они не имеют отношения к аппаратным и основаны на системных тиках, т.е. разрешение таймера напрямую зависит от настроек в файле конфигурации системы FreeRTOSConfig.h
. Тем не менее, работают они по сути так же: программист задает некоторое время, по истечении которого таймер вызывает функцию, которая исполняет некоторый код. Это чем-то похоже на прерывание, но оным не является, так как это обычная функция. При этом правило ее использования такое же, как и у прерывания: код должен выполняться как можно быстрее.
Программные таймеры могут находиться в одном из двух состояний: пассивном (англ. dorman state) или активном (англ. active state). В пассивном режиме таймер не ведет подсчет времени и, соответственно, не может вызвать исполнение кода, когда время истекло. Ниже приведена диаграмма переходов.

По ней видно, что режимов работы самого таймера два: интервальный (англ. one-shot timer) — отсчитав один раз, он переходит из активного состояния в пассивное; и периодический (англ. auto-reload timer) — после отработки одного цикла запускается следующий. По сути это облегченная версия задачи.
Как и любой другой объект, таймеры могут быть созданы, запущены, остановлены и удалены.
Таймеры не являются частью ядра.
#include "timers.h"
TimerHandle_t xTimerLedToggle;
uint32_t uiTimerLedTogglePeriod = 1000 / portTICK_RATE_MS;
void vTimerLedToggle(TimerHandle_t xTimer) {
LED_TOGGLE();
uiTimerLedTogglePeriod += 1 / portTICK_RATE_MS;
xTimerChangePeriod(xTimer, uiTimerLedTogglePeriod, 0);
}
int main( void ) {
// create periodic timer
xTimerLedToggle = xTimerCreate(
"LedToggleTimer", // timer name
uiTimerLedTogglePeriod, // period
pdTRUE, // auto-reload mode
0, // ID
vTimerLedToggle); // user callback function, handler
// ...
xTimerStart(xTimerLedToggle, 0);
vTaskStartScheduler();
while(1) {
}
}
Данный пример (весьма искусственный) иллюстрирует все основные возможности программного таймера. Через 1 секунду после его запуска произойдет вызов функции vTimerLedToggle()
, которая изменит состояние светодиода и увеличит период на 1 мс, после чего завершит свое выполнение. Следующий цикл продлится 1001 мс, совершив те же самые действия. Другие возможности таймера можно найти в официальной документации.
Если код, который нужно выполнять периодически, невелик, то имеет смысл вместо задачи использовать таймер. Это сэкономит некоторое количество оперативной памяти и при этом не окажет существенной нагрузки на планировщик.