Apostila de Programação com Arduino

8. FreeRTOS: Gerenciamento de Tasks

Assim como o nosso escalonador, o FreeRTOS também trabalha com o conceito de ticks e tarefas. O tick é a unidade de tempo discreta, gerada a partir de uma interrupção, que, por sua vez, pode ocasionar o chaveamento de uma tarefa para outra.

Diferentemente do nosso Task Switcher, o FreeRTOS admite preempção de tarefas. Ou seja, permite suspender a execução de uma tarefa e resumi-la mais adiante. Com isso, nossas tarefas podem ser implementadas como programas independentes, em um laço infinito.

8.1 Criando tarefas

Vamos demonstrar o escalonamento com preempção a partir de um programa bem simples, que cria duas tarefas. No código do main.c, vamos criar duas tarefas,

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

#define DELAY_LOOP_COUNT 100000000

void vTask1( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 1 is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

void vTask2( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 2 is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

que basicamente imprimem uma mensagem na tela e depois entram em um loop de espera ativa.

Para criar as tarefas, é possível usar a função xTaskCreate. Para tal, dentro do main(), insira o seguinte código:

int main( void )
{
    /* --- APPLICATION TASKS CAN BE CREATED HERE --- */
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL);
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL);
 
    /* Start the created tasks running. */
    vTaskStartScheduler();
 
    /* Execution will only reach here if there was insufficient heap to
    start the scheduler. */
    for( ;; );
    return 0;
}

onde os argumentos, em ordem, são: ponteiro para a função (vTask1), nome da função ("Task 1"), tamanho do stack em bytes (1000), argumentos para a tarefa (NULL), a prioridade (1) e um handler para a tarefa (NULL). Mais tarde estudaremos a influência da prioridade; os demais parâmetros podem ser consultados na documentação do FreeRTOS.

Ao compilar e executar este código, temos a seguinte saída:

Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
...

o que indica que o escalonador está dividindo o tempo entre as tarefas, de modo a dar a aparência de execução simultânea.

Realmente, quando executamos duas tarefas de mesma prioridade, o FreeRTOS faz o chamada time slicing, chaveando a tarefa em execução a cada tick, como mostrado na Fig.:

onde cada divisão de tempo corresponde ao intervalo dos ticks. Na prática, o que ocorre é que, a cada interrupção de tick, o kernel é chamado para determinar a tarefa a ser executada, como mostrado na Fig.:

8.2 Prioridade de tarefas

Para determinar a tarefa a ser executada, o kernel sempre escolhe a de maior prioridade. Caso haja empate, ele faz o time slicing, como mostrado no exemplo anterior.

Vamos testar essa funcionalidade. Modifique a seguinte linha da aplicação anterior:

xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL);

para:

xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, NULL);

Isto é, altere a prioridade da tarefa 2 para 2, que agora é maior do que a da tarefa 1.

Ao compilar e executar essa aplicação, temos a seguinte saída:

Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
...

que corresponde ao seguinte gráfico de execução:

Ou seja, apenas a tarefa 2 está sendo executada. Isto é esperado, pois a sua prioridade é maior e, portanto, é sempre escolhida pelo escalonador.

8.3 Usando o estado bloqueado para criar atrasos

Você deve estar se perguntando agora: como fazer para que a tarefa 1 execute, uma vez que esta possui prioridade inferior? Para responder esta pergunta, precisamos introduzir o conceito de estado de uma tarefa, mostrados na seguinte máquina de estados:

Observando os estados, verificamos que tarefas bloqueadas não podem ser selecionadas pelo kernel para execução. Assim, podemos atualizar o comportamento do escalonador: a tarefa a ser escolhida é a de maior prioridade entre as tarefas não bloqueadas. Caso possua mais de uma, é feito o time slicing (como visto na seção 8.1).

Uma forma de bloquear uma tarefa é criar um atraso (delay) com a função vTaskDelay, que recebe a duração em número de ticks como argumento. Para adaptar a nossa aplicação, vamos modificar as tarefas:

void vTask1( void *pvParameters )
{
  const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
  for( ;; )
  {
    printf("Task 1 is running\n");
    vTaskDelay(xDelay1000ms);
  }
}

void vTask2( void *pvParameters )
{
  const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
  for( ;; )
  {
    printf("Task 2 is running\n");
    vTaskDelay(xDelay1000ms);
  }
}

onde a função pdMS_TO_TICKS é utilizada para fazer a conversão de milissegundos para número de ticks. Ou seja, vTaskDelay(xDelay1000ms) cria um atraso de 1s.

A saída do programa é:

Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
...

que é semelhante a da versão com time slicing. No entanto, o gráfico de execução é bem diferente, como pode ser visto na Fig.:

Observe que, uma vez que todas as tarefas estão bloqueadas, a tarefa idle, que é uma tarefa criada automaticamente e basicamente não faz nada, é executada.

Comparando a execução da aplicação da seção 8.1 com o desta seção, nota-se que a última é muita mais eficiente, pois ocupa apenas uma fração do processador com as tarefas criadas. Em contraste, a aplicação anterior utilizava 100% do processador para executar as duas tarefas definidas.

8.4 Criando tarefas periódicas

A função de atraso da seção anterior permite criar um intervalo definido entre o final de uma iteração da tarefa e o início da próxima iteração. Para criar uma tarefa realmente periódica, em que o início de cada iteração ocorre em intervalos regulares, como o do capítulo anterior, podemos utilizar a função vTaskDelayUntil().

Para ilustrar melhor esta funcionalidade, vamos usar as duas tarefas contínuas executando em time slicing da seção 8.1.

#define DELAY_LOOP_COUNT 100000000
void vTask1( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 1 (continuous) is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

void vTask2( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 2 (continuous) is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

Além dessas, vamos criar uma tarefa periódica:

void vTask3( void *pvParameters )
{
  const TickType_t xDelay2000ms = pdMS_TO_TICKS( 2000 );
  TickType_t xLastWakeTime;
  xLastWakeTime = xTaskGetTickCount();
 
  for( ;; )
  {
    printf("Task 3 (periodic) is running\n");
    vTaskDelayUntil(&xLastWakeTime, xDelay2000ms);
  }
}

Para a tarefa periódica, devemos criar a variável xLastWakeTime para armazenar o instante de tempo da última execução. A função vTaskDelayUntil() recebe esta informação e a duração do atraso, que será de 2s. Além de realizar o delay, esta função atualiza automaticamente a variável xLastWakeTime.

A execução da aplicação deve render a seguinte saída:

Task 3 (periodic) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 3 (periodic) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
...

Cujo diagrama de execução é exibido a seguir:

Bibliografia para este capítulo