FreeRTOS - совместный доступ к общим ресурсам

Продолжаем осваивать FreeRTOS.

Потенциальная причина ошибок в RTOS — это неправильно организованный общий (совместный) доступ к ресурсам из нескольких задач и/или прерываний. Одна задача получает доступ к ресурсу, начинает выполнять некоторые действия с ним, но не успевает завершить операции с этим ресурсом до конца как в этот момент может произойти:

  • в случае с вытесняющей многозадачностью — переключение контекста задачи

  • в любой системе даже без RTOS — прерывание

Если другая задача или обработчик возникшего прерывания обратятся к этому же самому ресурсу, состояние которого носит промежуточный характер из-за воздействия первой задачи, то результат работы программы очень сложно предугадать. Баги, связанные с многозадачностью потенциально очень коварны, так как при тестировании могут проявляться почти случайным образом и тут многое зависит от внимательности и квалификации программиста.

В нашем случае левые и правые RGB светодиоды имеют общие порты P4OUT, P5OUT и P6OUT — биты 0..4 за левыми, а 5..7 за правыми. Аналогичная ситуация с портом P3OUT, где два младших бита за дисплеем, а остальные 6 для двух правых RGB светодиодов. Проблема заключается в том, что последовательность действий над портами с очень большой долей вероятности является не атомарной, поскольку занимает больше одной инструкции до полного завершения, и соответственно может быть прервана посередине. Например пусть стоит задача установить (сбросить, инвертировать — не имеет значения) один бит в регистре порта ввода/вывода. Большинство компиляторов транслирует такой код в несколько последовательных инструкций ассемблера:

  • копирование значения порта микроконтроллера в регистр общего назначения

  • модификация (сбросить, инвертировать и т.д.) регистра общего назначения

  • обратное копирование результата из регистра общего назначения в порт

Задача А загружает значение порта в регистр общего назначения --> в этот момент ее вытесняет задача Б, при этом задача А не «успела» модифицировать и записать данные обратно в порт --> задача Б в свою очередь тоже изменяет значение порта и блокируется --> задача А продолжает выполняться с точки, в которой ее выполнение было прервано. Какое значение будет записано в порт ? Явно не то, что задумывал программист.

MSP430.js | исходники

screenshot

Для того, чтобы обеспечить целостность данных в любой момент времени доступ к ресурсу, который является общим для задач, либо общим для задач и прерываний, в RTOS предусмотрен механизм взаимного исключения (mutual exclusion). Этот механизм гарантирует, что если задача начала выполнять некоторые действия с ресурсом, то никакая другая задача (или прерывание) не сможет получить доступ к данному ресурсу, пока операции с ним не будут завершены первой задачей.

FreeRTOS предоставляет несколько возможностей, которые можно использовать для реализации взаимного исключения — критические секции, мьютексы, задачи-сторожа. Критические секции — это очень грубый способ реализации взаимного исключения, т.к. они работают либо просто путем запрета прерываний либо приостановкой всего планировщика. Лучше по мере возможности (использование мьютекса из тела обработчика прерывания невозможно) использовать мьютексы — для этого нужно в файле конфигурации FreeRTOSConfig.h добавить макроопределение #define configUSE_MUTEXES 1 и также мьютекс должен быть явно создан перед первым его использованием например xP4P5P6Mutex = xSemaphoreCreateMutex(); в main.c перед запуском планировщика. Макроопределение #define INCLUDE_vTaskSuspend 1 опционально — если определено, то при использовании константы portMAX_DELAY задача может находиться в блокированном состоянии сколь угодно долго. Это касается не только мьютексов, но и других API FreeRTOS — например очередей, которые будут рассмотрены в следующем материале.

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

task_l_leds.h

void vTaskLedsL( void *pvParameters );

#include <FreeRTOS.h>
#include <semphr.h>

extern volatile xSemaphoreHandle xP4P5P6Mutex;

task_l_leds.c

#include <FreeRTOS.h>
#include <task.h>

#include "task_l_leds.h"

volatile xSemaphoreHandle xP4P5P6Mutex;
static portTickType xLastWakeTime;

static void rotate_rgb(volatile uint8_t * const port) {
    for (uint8_t i = 0; i < 6; i++) {

      /* Attempt to take the mutex, blocking indefinitely to wait for the mutex */
      xSemaphoreTake( xP4P5P6Mutex, portMAX_DELAY );

      *port = (*port & ~0b11111) | (0b10000 >> i);

      /* The mutex MUST be given back! */
      xSemaphoreGive( xP4P5P6Mutex );

      vTaskDelayUntil( &xLastWakeTime, 5 /* ticks */ );
    }
}

void vTaskLedsL( void *pvParameters ) {

   xLastWakeTime = xTaskGetTickCount();

   for( ;; ) {
     rotate_rgb(&P4OUT);
     rotate_rgb(&P5OUT);
     rotate_rgb(&P6OUT);
   }

   vTaskDelete( NULL );
}

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

В других задачах с анимациями любые модификации разделяемых портов ввода/вывода тоже необходимо пропускать через мьютекс — например xSemaphoreTake( xP4P5P6Mutex, portMAX_DELAY ); P4OUT |= ~0b11111; xSemaphoreGive( xP4P5P6Mutex );

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

Далее обмен данными между задачами.

links

social