ÍNDICE DE CONTEÚDO
Este artigo é talvez o mais importante e a base de tudo o que veremos pela frente, leia com atenção. Vamos começar definindo alguns termos do scheduler e, logo depois, como uma tarefa se comporta dentro de um RTOS. Pode ser que o entendimento de ambas partes faça mais sentido para você em ordem inversa, então leia sobre as tarefas antes do scheduler.
Scheduler
Scheduler (agendador ou escalonador) é o grande responsável por administrar as tarefas que irão obter o uso da CPU. Há diversos algoritmos para que o scheduler decida a tarefa e você também pode escolher o mais apropriado ao seu embarcado, como por exemplo: RR (Round Robin), SJF (Shortest Job First) e SRT (Shortest Remaining Time). Não entraremos em detalhes sobre eles.
Formas de trabalho do scheduler
Preemptivo: São algoritmos que permitem uma tarefa em execução ser interrompida antes do tempo total de sua execução, forçando a troca de contexto. Os motivos de interrupção são vários, desde uma tarefa com maior prioridade ou o Time Slicing (explicado logo abaixo):
Podemos observar na figura 1 como a preempção e Time Slicing funciona. As tarefas são interrompidas pois há tarefas com maiores prioridades prontas para serem executadas.
Embarcados Experience 2024: Evento Presencial
Participe do Embarcados Experience 2024 em São Paulo. Conhecimento técnico, palestras, workshops e oportunidade de networking com profissionais experientes.
A tarefa “Idle” sempre estará presente, onde o RTOS executa alguns gerenciamentos do sistema, como gerenciamento de memória RAM. É a tarefa de menor prioridade (0), logo, todas suas tarefas restantes no sistema são aconselhadas a terem prioridade maior ou igual a 1.
Cooperativo: São algoritmos que não permitem uma tarefa em execução ser interrompida, as tarefas precisam cooperar para que o sistema funcione. A tarefa em execução continuará em execução até que seu tempo total de execução termine e ela mesmo force a troca de contexto, assim permitindo que outras tarefas obtenham uso do CPU.
Podemos observar na figura 2, caso outras tarefas estejam prontas para serem executadas, não serão pelo simples fato de que a tarefa em atual execução deve exigir a troca de contexto ou terminar sua execução.
Troca de contexto: É o ato do S.O. salvar ou recuperar o estado do CPU, como registradores, principalmente o IP (Instruction Pointer). Isso permite que o S.O. retome o processamento da tarefa de onde foi interrompida.
Time Slicing: É o ato do S.O. dividir o tempo de uso do CPU entre as tarefas. Cada tarefa recebe uma fatia desse tempo, chamado quantum, e só é permitida ser executada por no máximo o tempo de um quantum. Se após o término do quantum a tarefa não liberou o uso do CPU, é forçada a troca de contexto e o scheduler será executado para decidir a próxima tarefa em execução. Caso não haja outra tarefa para ser escolhida, incluindo motivos de prioridade, o scheduler retornará para a mesma tarefa anterior à preempção. Você entenderá melhor ao decorrer deste post.
Ao término de todos quantum’s, é efetuada a troca de contexto e o scheduler decidirá a próxima tarefa em execução, perdendo um pequeno tempo para si, já que irá interromper a atual tarefa em execução. O Time Slicing é indicado com tarefas de mesma prioridade, já que sem o uso, as tarefas de mesma prioridade terão tempos de execução diferentes. No ESP32 em 240 MHz, o tempo para o scheduler decidir a tarefa e esta entrar em execução é <=10 us.
No FreeRTOS, o período do Time Slicing pode ser configurado, sendo aconselhável valores entre 10 ms e 1 ms (100-1000 Hz), cabe a você escolher o que melhor atende ao seu projeto, podendo ultrapassar os limites aconselháveis.
Em todos artigos desta série, utilizaremos o FreeRTOS preemptivo com Round Robin e Time Slicing em 1000 Hz, que é o padrão do nosso microcontrolador ESP32.
Tarefas
As tarefas (Task) são como mini programas dentro do nosso embarcado, normalmente são loops infinitos que nunca retornarão um valor, onde cada tarefa efetua algo específico. Como já visto no scheduler, ele que fará todas nossas tarefas serem executadas de acordo com sua importância, e assim, conseguimos manter inúmeras tarefas em execução sem muitos problemas.
Em embarcados de apenas uma CPU, só existirá uma tarefa em execução por vez, porém, o scheduler fará a alternância de tarefas tão rapidamente, que nos dará a impressão de que todas estão ao mesmo tempo.
Prioridades
Toda tarefa tem sua prioridade, podendo ser igual a de outras. A prioridade de uma tarefa implica na ordem de escolha do scheduler, já que ele sempre irá escolher a tarefa de maior prioridade para ser executada, logo, se uma tarefa de alta prioridade sempre estiver em execução, isso pode gerar problemas no seu sistema, chamado Starvation (figura 4), já que as tarefas de menor prioridade nunca executarão até que sejam a de maior prioridade para o scheduler escolher. Por esse motivo, é sempre importante adicionar delay’s no fim de cada tarefa, para que o sistema tenha um tempo para “respirar”.
A menor prioridade do FreeRTOS é 0 e aumentará até o máximo explícito nos arquivos de configuração. Uma tarefa mais prioritária é a que têm o número maior que a outra, por exemplo na figura 3 veja as prioridades:
- Idle task: 0;
- Task1: 1;
- Task2: 5.
A tarefa de maior prioridade é a ”Task2” e a menor é a ”Idle task”.
No caso de tarefas com mesma prioridade, nosso scheduler com Round Robin e Time Slicing tentará deixar todas tarefas com o mesmo tempo de uso da CPU, como mostrado na figura 4.
Estados
As tarefas sempre estarão em algum estado. O estado define o que a tarefa está fazendo dentro do RTOS no atual tempo de análise do sistema.
Bloqueada (Blocked): Uma tarefa bloqueada é quando está esperando que algum dos dois eventos abaixo ocorram. Em um sistema com muitas tarefas, a maior parte do tempo das tarefas será nesse estado, já que apenas uma pode estar em execução e todo o restante estará esperando por sua vez, no caso de sistemas single core.
- Temporal (Timeout): Um evento temporal é quando a tarefa está esperando certo tempo para sair do estado bloqueado, como um Delay. Todo Delay dentro de uma tarefa no RTOS não trava o microcontrolador como em Bare Metal, o Delay apenas bloqueia a tarefa em que foi solicitado, permitindo todas outras tarefas continuar funcionando normalmente.
- Sincronização (Sync): Um evento de sincronização é quando a tarefa está esperando (Timeout) a sincronização de outro lugar para sair do estado bloqueado, como Semáforos e Queues (serão explicados nos próximos artigos).
Suspensa (Suspended): Uma tarefa suspensa só pode existir quando for explicitamente solicitada pela função “vTaskSuspend()” e só pode sair desse estado também quando solicitado por “vTaskResume()”. É uma forma de desligar uma tarefa até que seja necessária mais tarde, entretanto, é pouco usado na maioria dos sistemas simples.
Pronta (Ready): Uma tarefa está pronta para ser executada quando não está nem bloqueada, suspensa ou em execução. A tarefa pode estar pronta após o Timeout de um Delay acabar, por exemplo, entretanto, ela não estará em execução e sim esperando que o scheduler à escolha. Este tempo para a tarefa ser escolhida e executar pode variar principalmente pela sua prioridade.
Execução (Running): Uma tarefa em execução é a tarefa atualmente alocada na CPU, é ela que está em processamento. Se seu embarcado houver mais de uma CPU, haverá mais de uma tarefa em execução ao mesmo tempo. O ESP32 conta com 3 CPU’s, entretanto, apenas 2 podem ser usadas pelo FreeRTOS na IDF, com isso, temos no máximo duas tarefas em execução ao mesmo tempo e o restante estará nos outros estados.
Observe na figura 5 o ciclo de vida de uma tarefa, atente-se que apenas tarefas prontas para execução podem entrar em execução diretamente. Uma tarefa bloqueada ou suspensa nunca irá para execução diretamente.
Agora que já sabemos como funciona a base do scheduler e suas tarefas, vamos finalmente botar a mão na massa e testar essa maravilha funcionando na prática. Como já foi dito, vamos utilizar o ESP32 com a IDF e FreeRTOS para nossos testes, mas você pode testar em seu embarcado como ARM, talvez mudando apenas alguns detalhes!
Vamos fazer um teste simples com três tarefas para mostrar os principais itens acima. O princípio dessas três tarefas é apenas mostrar duas tarefas com mesma prioridade dividindo o uso da CPU enquanto a terceira tarefa é executada poucas vezes para mostrar a preempção.
Código do projeto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#include <driver/gpio.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <esp_system.h> void t1(void*z) {//Tarefa que simula PWM para observar no analisador logico while (1) { gpio_set_level(GPIO_NUM_2, 1); ets_delay_us(25); gpio_set_level(GPIO_NUM_2, 0); ets_delay_us(25); } } void t2(void*z) {//Tarefa que simula PWM para observar no analisador logico while (1) { gpio_set_level(GPIO_NUM_4, 1); ets_delay_us(25); gpio_set_level(GPIO_NUM_4, 0); ets_delay_us(25); } } void t3(void*z) {//Tarefa que simula PWM para observar no analisador logico while (1) { for (uint8_t i = 0; i < 200; i++) { gpio_set_level(GPIO_NUM_15, 1); ets_delay_us(500); gpio_set_level(GPIO_NUM_15, 0); ets_delay_us(500); } vTaskDelay(pdMS_TO_TICKS(200)); } } void app_main() { //Seleciona os pinos que serao usados gpio_pad_select_gpio(GPIO_NUM_2); gpio_pad_select_gpio(GPIO_NUM_4); gpio_pad_select_gpio(GPIO_NUM_15); //Configura os pinos para OUTPUT gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT); gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT); gpio_set_direction(GPIO_NUM_15, GPIO_MODE_OUTPUT); //Cria as tarefas xTaskCreatePinnedToCore(t1, "task1", 2048, NULL, 1, NULL, 0);//Tarefa 1 com prioridade UM (1) no core 0 xTaskCreatePinnedToCore(t2, "task2", 2048, NULL, 1, NULL, 0);//Tarefa 2 com prioridade UM (1) no core 0 xTaskCreatePinnedToCore(t3, "task3", 2048, NULL, 2, NULL, 0);//Tarefa 3 com prioridade DOIS (2) no core 0 } |
Primeiramente, vamos analisar o que a teoria acima nos diz. Com duas tarefas compartilhando a mesma prioridade, nosso gráfico irá se comportar igual à figura 4, entretanto, nós temos uma terceira tarefa que tem prioridade maior que as outras duas. Essa última é executada menos frequentemente, porém por mais tempo que as outras. Nosso gráfico deve ficar parecido com a figura 6:
Observe que as duas tarefas de prioridades iguais (Task1 e Task2) compartilham o tempo de uso do CPU enquanto a outra tarefa (Task3) permanece bloqueada por um delay. Logo que a Task3 está pronta para ser executada, o scheduler irá executá-la até que encontre o delay novamente, que é quando a tarefa fica bloqueada, permitindo tarefas de prioridade menor serem executadas. Lembre-se que delay não trava todo o microcontrolador, apenas a tarefa em que foi solicitado.
Ok, vamos ver se a teoria bate com a prática? Cada tarefa faz um simples Toggle em um pino nos permitindo analisar com o Analisador Lógico nas figuras 7,8 e 9.
As tasks 1 e 2 compartilham o uso do CPU até que a task3 seja desbloqueada e, quando isso ocorre, a task3 que tem maior prioridade, será executada até o término do seu código (quando encontra o delay). Observe o tempo de 200 ms no canto superior direito da figura 7, é nosso delay de 200 ms da tarefa 3, ou seja, ela realmente permaneceu bloqueada por 200 ms.
Dando um zoom, podemos observar as duas tarefas “brigando” pela CPU, veja na figura 8. No canto superior direito da figura 8, a tarefa usa a CPU por 1 ms, que é o tempo do Time Slicing configurado em 1000 Hz.
Observando o gráfico numa base de tempo “humana” (figura 9), para nós parece que ambas estão executando simultaneamente, entretanto, podemos provar que as duas compartilham a CPU (figura 8), atente-se a isso em seus projetos de “Time Critical”.
Podemos ir mais longe já que nosso microcontrolador permite 2 das 3 CPU’s serem usadas pelo FreeRTOS. Atribuindo a tarefa 3 no outro CPU (1), podemos ver na figura 9 que ela executará realmente em simultâneo enquanto as tarefas 1 e 2 “brigam” pelo CPU (0).
No próximo artigo desta série vamos abordar os semáforos, que funcionam para sincronizar eventos e tarefas dentro do RTOS.
Saiba mais
Desenvolvendo um RTOS: Introdução
Implementando elementos de RTOS no Arduino
Criando um projeto no IAR com o FreeRTOS
Referências
Eu consigo implementar uma nova política de escalonamento para o FreeRTOS? Caso afirmativo, como eu posso fazer isso?
tentei executar este código através da interface ide arduino e obtenho constantemente a seguite mensagem no monitor serial da ide arduino: E (10162) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time: E (10162) task_wdt: – IDLE0 (CPU 0) E (10162) task_wdt: Tasks currently running: E (10162) task_wdt: CPU 0: task3 E (10162) task_wdt: CPU 1: loopTask E (10162) task_wdt: Aborting. abort() was called at PC 0x400d8a03 on core 0 ELF file SHA256: 0000000000000000 Backtrace: 0x40084b18:0x3ffbe3f0 0x40084d8d:0x3ffbe410 0x400d8a03:0x3ffbe430 0x4008301d:0x3ffbe450 0x4000c04d:0x3ffb27a0 0x400d0c54:0x3ffb27c0 0x40085d6d:0x3ffb27e0 Rebooting… ets Jun 8 2016 00:22:57 rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee… Leia mais »
Olá Mateus! Sua tarefa ocupou tempo demais da CPU0 e acabou fazendo com que o Task Watchdog indicasse erro. Você pode solucionar adicionando um pequeno delay na sua tarefa, para deixar o resto do sistema “respirar”
Olá
Algo como vTaskDelay(pdMS_TO_TICKS(1)) em t1 e t2?
Sim, com isso você vai permitir que a task IDLE0 (prioridade 0) rode e não acione o TWDT.
Muito bom. Belo artigo.
O Zé é meu amigo
Qual ambiente de programação que vc está usando??
Visual Code, porém, utilizava Eclipse também.