Продолжаем осваивать FreeRTOS.
Самый простой и интуитивно понятный способ организовать обмен информацией между задачами — использовать общие глобальные переменные и для кооперативной многозадачности такой подход вполне уместен. Однако в случае с вытесняющий многозадачностью при совместном доступе нескольких задач к общей переменной возникают проблемы атомарности и поэтому во FreeRTOS для передачи информации между задачами придумали очереди. Очереди представляют собой фундаментальный механизм FreeRTOS, который лежит в основе различного рода взаимодействий задач друг с другом. Очереди могут быть использованы для передачи информации как между задачами, так и между прерываниями и задачами. Если сравнивать с глобальными переменными, то помимо решения проблем с атомарностью очередь обладает рядом дополнительных возможностей — блокировка на чтение/запись, позволяющая задаче не расходовать процессорное время на периодически опрос глобальной переменной, размер очереди не ограничен 1, данные можно класть либо в начало либо в конец очереди.
В рассматриваемом примере одна задача будет сканировать кнопки и передавать данные о нажатии двум другим задачам светодиодных анимаций с помощью двух очередей.
Очередь может хранить в себе конечное число элементов фиксированного размера.
Максимальное число элементов, которое может хранить очередь, называется размером очереди. Как размер элемента, так и размер очереди задаются при создании очереди и остаются неизменными до ее удаления. В нашем случае перед запуском планировщика в файле 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 говорит о том, что таймаут завершен, а никакая другая задача или прерывание так и не записали элемент в очередь
Всё бы хорошо, оно работает но есть одно но — зачем нам вообще отдельная задача для считывания кнопок ? Ведь она постоянно крутится в памяти микроконтроллера и потребляет ресурсы даже когда не происходит никакой активности со стороны пользователя. Куда более оптимальным решением было бы использовать для подобных целей прерывание, вызываемое при нажатии на кнопку.
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
не дожидаясь окончания текущего кванта времени планировщика. Гибридная подход позволяет сократить время реакции системы на прерывание.