Implementando elementos de RTOS no Arduino

RTOS no Arduino

Os projetos embarcados são, em sua maioria, compostos por diversas tarefas executadas para atender uma aplicação de propósito único. Dependendo da complexidade do sistema, tais tarefas podem exigir restrições de tempo, além de interagirem umas com as outras. Muitas vezes, o uso de arquiteturas mais simples, como orientada a interrupção ou máquina de estados, tornam o código muito complexo e de difícil manutenção. Devido a isso, os sistemas operacionais de tempo real (RTOS – Real Time Operating System) se tornam uma boa opção, pois são capazes de atender as restrições temporais e tornar o código mais organizado e flexível.

Basicamente, um RTOS é um sistema operacional destinado a gerenciar múltiplas tarefas com períodos de execução pré-definidos. Este gerenciamento é feito pelo escalonador (ou scheduler em inglês) baseado em algum critério, como tempo máximo de execução, prioridade, criticidade de um evento, sequência de execução, entre outros. Em razão disso, define-se dois tipos de escalonadores: o escalonador preemptivo, que é capaz de interromper uma determinada tarefa para executar outra de maior prioridade, e o escalonador cooperativo (ou não-preemptivo) que executa cada tarefa até o fim, de modo que a próxima tarefa só é executada quando a anterior terminar.

Existem diversos RTOS prontos, alguns gratuitos e outros pagos. No entanto, dependendo da relação custo/benefício do projeto, pode não ser viável utilizar um deles. Assim, se torna necessário criar nosso próprio software que consiga atender as necessidades de tempo real. Em razão disso, este artigo tem como objetivo exemplificar uma implementação de elementos de um RTOS em um código embarcado num Arduino. O código foi escrito totalmente em C, evitando-se o uso demasiado de rotinas prontas da IDE do Arduino, para facilitar a migração para outras plataformas.

Problemas de restrições de tempo com arquiteturas mais simples

Para melhor explicar esta parte, vamos supor que temos um sistema embarcado que deve ser capaz de monitorar e controlar uma planta industrial e manter uma interface com o operador através de um display LCD e de um teclado matricial.

Se estas tarefas não tivessem restrições temporais, a maneira mais simples de implementá-las seria através de um loop infinito, de forma sequencial. Por exemplo:

Vamos supor agora que elas devem ser executadas a cada 100 ms. Ainda assim o código se manteria simples, bastando acrescentar um delay após todas as tarefas serem executadas ou, melhor ainda, utilizar uma arquitetura baseada num temporizador configurado para estourar a cada 100 ms. Dessa forma, o código ficaria assim:

Entretanto, quando as tarefas passam a exigir períodos de execução diferentes, o loop infinito torna-se inviável. Mas, é possível, utilizando uma base de tempo provinda do temporizador, resolver este problema de uma maneira simples, desde que o sistema não seja rigoroso (conhecido como Hard Real Time Systems).

Sistemas de tempo real rigorosos são aqueles que, sem exceção, devem satisfazer a todas as suas restrições temporais – se uma restrição é violada, o sistema pode causar grandes danos.

Voltando ao problema, se utilizarmos a interrupção do temporizador para gerar uma base de tempo conhecida e compararmos com o período de tempo para execução de cada tarefa, conseguimos identificar quando cada uma delas deve ser executada. Para isso, basta criar uma variável que armazena o tempo num determinado momento e uma que contenha o tempo atual conforme a base do temporizador. Quando a diferença entre elas exceder o período de uma determinada tarefa, significa que ela deve ser executada. Colocando isso em prática, tem-se o seguinte código:

Na IDE do Arduino já existe uma função que retorna o tempo em ms decorrido desde o momento em que o código começou a rodar, ela é chamada de millis(). A implementação acima utilizando esta função ficaria assim:

Agora, imaginem como ficaria o código se o sistema tivesse muito mais tarefas. Esta abordagem, apesar de funcional, acaba deixando o código bastante poluído. Então, o passo seguinte é organizar estes conceitos em um único código capaz de adicionar e remover tarefas, e gerencia-las conforme seus períodos. Este algoritmo é conhecido como gerenciador de tarefas e é parte principal do Kernel de um RTOS.

De forma resumida, um Kernel é o núcleo central de um sistema operacional. Além dele gerenciar as tarefas, ele também gerencia a memória disponível e intermedia a comunicação entre os drivers de hardware e as aplicações. No entanto, neste artigo considerou-se um “microkernel” responsável apenas pelo gerenciamento das tarefas, como pode ser visto na implementação a seguir.

Desenvolvimento do kernel

Como mencionado, o Kernel deve ser capaz de armazenar as tarefas a serem executadas. Uma maneira simples de fazer isso é utilizando um buffer estático, do tipo ponteiro de função, que salva os endereços das funções correspondentes às tarefas (mais detalhes sobre o uso de ponteiro de função podem ser vistos aqui). Além disso, precisamos informar qual o período da tarefa e, opcionalmente, o seu nome para debug e se ela está habilitada ou não para ser executada.

Este Kernel é composto por quatro funções: uma para inicializa-lo, uma para adicionar tarefas, uma para remover tarefas e outra para rodar o escalonador. Em geral, a função que roda o escalonador entra num loop infinito e, conforme a base de tempo gerada pelo Timer, verifica qual tarefa deve ser executada no momento, de acordo com seu período. Para entender melhor, vamos ao código. Abaixo, o header (arquivo.h) do projeto:

E a implementação (arquivo .c):

Aplicação

Para exemplificar uma aplicação do Kernel desenvolvido, criou-se três tarefas:

  • vDisp7SegTask(): tarefa responsável por implementar um contador de 500 ms, através da multiplexação de um display de 7 segmentos quádruplo;
  • vDispLcdTask(): tarefa responsável por implementar um contador de 1s num display lcd 16×2;
  • vTecladoTask(): tarefa responsável por ler dois pushbuttons. Quando pressionados, eles invertem o sentido da contagem dos contadores. Cada pushbutton controla um contador diferente.

Hardware

O hardware é composto por uma placa Arduino MEGA 2560, um display LCD 16×2, um display quádruplo de sete segmentos, dois push buttons, alguns resistores e alguns transistores BJT. A figura abaixo mostra o esquemático completo do circuito:

Software

O código a seguir apresenta o exemplo para a placa Arduino Mega 2560:

Funcionamento

Conclusão

O presente artigo propôs o desenvolvimento de um kernel para gerenciamento de tarefas de tempo real, de uma forma bem simples para facilitar o entendimento de quem está iniciando no mundo dos RTOS. Existem diversas melhorias que podem ser aplicadas nesta abordagem, como por exemplo mudar o buffer estático para um buffer circular, de modo que seja mais simples adicionar ou remover tarefas com o sistema em andamento. Outra dica é melhorar o algoritmo do escalonador, já que o atual gera uma certa sobrecarga verificando a diferença entre o tempo atual e o último tempo de execução para cada tarefa. Ademais, verificou-se que o código desenvolvido apresentou um bom desempenho, podendo ser usado em diversos projetos que necessite gerenciar múltiplas tarefas.

Download do código

Código completo do projeto aqui.

Referências

ALMEIDA, R. M. A.; MORAES, C. H. V.; SERAPHIM, T. F. P. Programação de Sistemas Embarcados: Desenvolvendo software para microcontroladores em linguagem C. Elsevier, 2016.

SHAW, A. C. Sistemas e software de tempo real. Bookman, 2003.

Desenvolvendo um RTOS: processos e tarefas

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Software » Implementando elementos de RTOS no Arduino
Comentários:
Notificações
Notificar
guest
7 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Vagner Rodrigues
04/11/2019 21:18

Parabéns, Caio. Muito bom. Tive uma dificuldade aqui, apenas abri um sketch novo e fiz a inclusão:
#include
Está aparecendo vários erros como este:
C:\Users\Usuario\Documents\Arduino\libraries\FreeRTOS\src\croutine.c:28:10: fatal error: C:\Users\Usuario\Documents\Arduino\libraries\FreeRTOS\src\Arduino_FreeRTOS.h: Permission denied

#include “Arduino_FreeRTOS.h”

^~~~~~~~~~~~~~~~~~~~

Já teve problema com isso?

Adrian Lemos
Adrian Lemos
02/05/2017 12:00

Caio, Bom dia

no seu segundo código no if da linha 19 não possui nenhuma base de comparação, é isso mesmo?

obrigado pelo artigo, mas não consegui passar direito dessa fase sem entender isso melhor.

Caio Moraes
Caio Moraes
Reply to  Adrian Lemos
02/05/2017 14:55

Bom dia Adrian,

É isso mesmo, quando não se usa um operador de comparação, a condição será verdadeira sempre que variável for verdadeira, ou seja, sempre que ela for diferente de zero.

Naquele caso, é o mesmo que fazer if(temporizadorEstourou == TRUE) { }

Valeu, abraço

Evandro Rech
Evandro Rech
05/04/2017 22:16

O artigo ficou muito bom! Estudei RTOS ano passado e comecei a procurar as opções para o Arduino para montar uma disciplina.

Fiquei com uma dúvida, como você faz a troca de contexto entre as tasks no scheduler? Geralmente vejo isso sendo feito em assembly mudando o stack pointer.

Obrigado!

Caio Moraes
Caio Moraes
Reply to  Evandro Rech
06/04/2017 10:57

Muito obrigado Evandro.
Comecei a estudar RTOS há pouco tempo e resolvi implementar este código como meio de estudo. Fiz esta abordagem bem simples, para quem está iniciando neste assunto entender alguns elementos do RTOS sem grandes dificuldades.

Diego Moreno
Diego Mendes Moreno
04/04/2017 16:29

Artigo muito legal! Ótima noção do funcionamento do Kernel em um contexto simples.

Só um comentário que no Gist de “arquivo .c” alguns nomes de funções ficaram comentados nas linhas 108 e 138.

Valeu!

Caio Moraes
Caio Moraes
Reply to  Diego Mendes Moreno
06/04/2017 10:49

Muito obrigado Diego.
Valeu pela observação, será corrigido

Talvez você goste:

Séries

Menu