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
- Tutorial e referência do FreeRTOS: https://www.freertos.org/Documentation/RTOS_book.html