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 16x2;
  • 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 16x2, 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

NEWSLETTER

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

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

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

Caio Moraes
Atualmente é mestrando em Engenharia Elétrica com ênfase em Eletrônica de Potência na Universidade Federal de Santa Catarina (UFSC). Possui graduação em Engenharia Elétrica pela Universidade Federal de Mato Grosso do Sul (UFMS) e é formado em Eletrotécnica com enfase em Controle e Processos Industriais, pela Escola Técnica Estadual (ETEC) de Ilha Solteira. Tem experiência e interesse nas áreas de eletrônica de potência, sistemas de controle e sistemas embarcados, mais especificamente nos temas modelagem dinâmica de conversores CC-CC, inversores conectados à rede e controle digital aplicado a conversores estáticos.

7
Deixe um comentário

avatar
 
4 Comment threads
3 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
5 Comment authors
Vagner RodriguesCaio MoraesAdrian LemosEvandro RechDiego Moreno Recent comment authors
  Notificações  
recentes antigos mais votados
Notificar
Vagner Rodrigues
Membro
Vagner Rodrigues

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
Visitante
Adrian Lemos

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
Visitante
Caio Moraes

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
Visitante
Evandro Rech

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
Visitante
Caio Moraes

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
Visitante
Diego Mendes Moreno

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
Visitante
Caio Moraes

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