FreeRTOS - обмен данными между задачами

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

Самый простой и интуитивно понятный способ организовать обмен информацией между задачами — использовать общие глобальные переменные и для кооперативной многозадачности такой подход вполне уместен. Однако в случае с вытесняющий многозадачностью при совместном доступе нескольких задач к общей переменной возникают проблемы атомарности и поэтому во FreeRTOS для передачи информации между задачами придумали очереди. Очереди представляют собой фундаментальный механизм FreeRTOS, который лежит в основе различного рода взаимодействий задач друг с другом. Очереди могут быть использованы для передачи информации как между задачами, так и между прерываниями и задачами. Если сравнивать с глобальными переменными, то помимо решения проблем с атомарностью очередь обладает рядом дополнительных возможностей — блокировка на чтение/запись, позволяющая задаче не расходовать процессорное время на периодически опрос глобальной переменной, размер очереди не ограничен 1, данные можно класть либо в начало либо в конец очереди.

В рассматриваемом примере одна задача будет сканировать кнопки и передавать данные о нажатии двум другим задачам светодиодных анимаций с помощью двух очередей.

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

screenshot

Очередь может хранить в себе конечное число элементов фиксированного размера. Максимальное число элементов, которое может хранить очередь, называется размером очереди. Как размер элемента, так и размер очереди задаются при создании очереди и остаются неизменными до ее удаления. В нашем случае перед запуском планировщика в файле main.c создаются две очереди xQueueBtnsL = xQueueCreate(1, sizeof(char)); для левых и правых кнопок соответственно. Поскольку у задачи, которая кладёт данные в очередь, приоритет ниже, чем у забирающей эти данные, то размера очереди в 1 элемент оказывается достаточно. При других раскладах приоритетов размер очереди возможно потребуется увеличить — в противном случае возможна ситуация, когда данные о нажатой кнопке не успели дойти до адресата, а кнопочку нажали заново и тогда сработает configASSERT т.к. в очереди закончилось место:

task_btn_scan.h

void vTaskScanBtns( void *pvParameters );

#include <queue.h>

extern volatile QueueHandle_t xQueueBtnsL, xQueueBtnsR;

task_btn_scan.c

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

#include "task_btn_scan.h"

volatile QueueHandle_t xQueueBtnsL, xQueueBtnsR;

void vTaskScanBtns( void *pvParameters ) {
  for( ;; ) {
    while (!P1IN); // button pushed

    if (P1IN & 0b1111) {

      /* Send a char to the queue. Don't block if the queue is already full 
        (the third parameter is zero, so not block time is specified). */
      configASSERT( xQueueSend( xQueueBtnsL, ( void * ) &P1IN, 0 ) == pdPASS );

    } else {
      configASSERT( xQueueSend( xQueueBtnsR, ( void * ) &P1IN, 0 ) == pdPASS );
    }

    while (P1IN);  // button released
  }

   vTaskDelete( NULL );
}

Для записи элемента в конец очереди используется API-функция xQueueSend(). В нашем случае на момент записи очередь всегда пуста, т.к. у задачи-получателя более высокий приоритет, поэтому таймаут (третий параметр) логично задать «0». Может возвращать 2 значения:

  • pdPASS — означает, что данные успешно записаны в очередь. Если таймаут не равен «0», то возврат значения pdPASS говорит о том, что свободное место в очереди появилось до истечения таймаута и элемент был помещен в очередь

  • errQUEUE_FULL — означает, что данные не записаны в очередь, так как очередь заполнена. Если таймаут не равен «0», то возврат значения errQUEUE_FULL говорит о том, что таймаут завершен, а место в очереди так и не освободилось

Задача-получатель информации о нажатии кнопки, которая заправляет анимациями левых светодиодов принимает следующий вид:

task_l_leds.c

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

#include "task_l_leds.h"
#include "task_btn_scan.h" // queues

volatile xSemaphoreHandle xP4P5P6Mutex;

// C99
// an object that has static storage duration is not initialized explicitly, then:
// — if it has pointer type, it is initialized to a null pointer;
static volatile uint8_t * port;

void vTaskLedsL( void *pvParameters ) {

   for( ;; ) {

      uint8_t button;

      // portMAX_DELAY will cause the task to wait indefinitely
      // (without timing out) provided INCLUDE_vTaskSuspend is set to 1
      portBASE_TYPE xStatus = xQueueReceive( 
                xQueueBtnsL, &button, port ? 10 : portMAX_DELAY );

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

      if ( xStatus == pdPASS ) {
        // set leds color
        switch (button) {
          case 0b10:   /* red blink */
            P5OUT &= ~0b11111; P6OUT &= ~0b11111; port = &P4OUT;
            break;
          case 0b100:  /* green blink */
            P4OUT &= ~0b11111; P6OUT &= ~0b11111; port = &P5OUT;
            break;
          case 0b1000: /* blue blink */
            P4OUT &= ~0b11111; P5OUT &= ~0b11111; port = &P6OUT;
            break;
          case 0b1:    /* off blink */
            P4OUT &= ~0b11111; P5OUT &= ~0b11111; P6OUT &= ~0b11111; port = NULL;
            break;  
        }
      }

      if (port) {
        *port = (*port & ~0b11111) | ((*port ^ 0b11111) & 0b11111);
      }

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

   vTaskDelete( NULL );
}

Для считывания элемента с удалением его из очереди используется API-функция xQueueReceive(). Таймаут portMAX_DELAY в паре с INCLUDE_vTaskSuspend задаёт бесконечное время ожидания. Также может возвращать 2 значения:

  • pdPASS — означает, что данные успешно прочитаны из очереди. Если таймаут не равен «0», то возврат значения pdPASS говорит о том, что элемент в очереди появился (или уже был там) до истечения тайм-аута и был успешно прочитан

  • errQUEUE_EMPTY — означает, что элемент не прочитан из очереди, так как очередь пуста. Если таймаут не равен «0», то возврат значения errQUEUE_FULL говорит о том, что таймаут завершен, а никакая другая задача или прерывание так и не записали элемент в очередь

Всё бы хорошо, оно работает но есть одно но — зачем нам вообще отдельная задача для считывания кнопок ? Ведь она постоянно крутится в памяти микроконтроллера и потребляет ресурсы даже когда не происходит никакой активности со стороны пользователя. Куда более оптимальным решением было бы использовать для подобных целей прерывание, вызываемое при нажатии на кнопку.

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

screenshot

btn_scan.c

#include "btn_scan.h"

volatile QueueHandle_t xQueueBtnsL, xQueueBtnsR;

void btn_scan_init() {
  // clear the interrupt flag
  P1IFG = 0;
  // enable interrupt on BIT 0..7
  P1IE  = 0xFF;
}

void __attribute__ ((interrupt(PORT1_VECTOR))) Port1_ISR (void);
void __attribute__ ((interrupt(PORT1_VECTOR))) Port1_ISR (void) {
  P1IFG = 0;  // clear the interrupt flag

  BaseType_t xHigherPriorityTaskWoken;

  /* xHigherPriorityTaskWoken must be initialised to pdFALSE. */
  xHigherPriorityTaskWoken = pdFALSE;

  if (P1IN & 0b1111) {

    /* xHigherPriorityTaskWoken will get set to pdTRUE if writing to the 
       queue causes a task to leave the Blocked state, and the task 
       leaving the Blocked state has a priority higher than the currently 
       executing task (the task that was interrupted). */

    configASSERT( xQueueSendFromISR( 
        xQueueBtnsL, ( void * ) &P1IN, &xHigherPriorityTaskWoken ) == pdPASS );
  } else {
    configASSERT( xQueueSendFromISR( 
        xQueueBtnsR, ( void * ) &P1IN, &xHigherPriorityTaskWoken ) == pdPASS );
  }

  /* If xHigherPriorityTaskWoken is now set to pdTRUE then a context
     switch should be requested. */
  portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

После оптимизации количество задач сократилось с 5 до 4 и соответственно освободилась RAM. Функцию xQueueSend() нельзя использовать в прерываниях — для этого есть xQueueSendFromISR().

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

links

social