Взаимодействие потоков

Стабильность работы системы напрямую зависит от взаимодействия задач. 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 мс, совершив те же самые действия. Другие возможности таймера можно найти в официальной документации.

Если код, который нужно выполнять периодически, невелик, то имеет смысл вместо задачи использовать таймер. Это сэкономит некоторое количество оперативной памяти и при этом не окажет существенной нагрузки на планировщик.


Изменено: