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:

void main()
{
   InicializaSistema();

   for(;;)
   { 
       ExecutaControlePlanta();
       LeTecladoMatricial();
       AtualizaDisplayLcd();
   }
}

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:

int8_t temporizadorEstourou;

void InterrupcaoDoTimer()
{
   temporizadorEstourou = TRUE;
}

void main()
{
   InicializaSistema();

   // Temporizador configurado para estourar a cada 100ms
   InicializaTemporizador();

   temporizadorEstourou = FALSE;
   
   for(;;)
   {
      if(temporizadorEstourou)
      {
         ExecutaControlePlanta();
         LeTecladoMatricial();
         AtualizaDisplayLcd();

         temporizadorEstourou = FALSE;
      }
   }  
}

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:

// Definição dos períodos de cada tarefa 
#define PERIODO_TAREFA1  10    //    10 ms
#define PERIODO_TAREFA2  50    //    50 ms
#define PERIODO_TAREFA3  100   //   100 ms

// Variáveis globais
uint32_t tempoAnteriorTarefa1;
uint32_t tempoAnteriorTarefa2;
uint32_t tempoAnteriorTarefa3;

volatile uint32_t sysTick;

// Trata interrupção do Timer
void InterrupcaoDoTemporizador()
{
   sysTick++;
}

// Rotina principal
void main()
{
   InicializaSistema();   

   // Temporizador configurado para estourar a cada 1ms
   InicializaTemporizador();
   
   // Inicialização das variáveis globais
   sysTick = 0;
   tempoAnteriorTarefa1 = 0;
   tempoAnteriorTarefa2 = 0;
   tempoAnteriorTarefa3 = 0;

   for(;;)
   {
      if((sysTick – tempoAnteriorTarefa1) >  PERIODO_TAREFA1) 
      {       
          ExecutaControlePlanta();
          tempoAnteriorTarefa1 = sysTick;
      }
     if((sysTick – tempoAnteriorTarefa2) >  PERIODO_TAREFA2) 
      {       
          LeTecladoMatricial();
          tempoAnteriorTarefa2 = sysTick;
      }
      if((sysTick – tempoAnteriorTarefa3) >  PERIODO_TASK3) 
      {       
          AtualizaDisplayLcd();
          tempoAnteriorTarefa3 = sysTick;
      }
   }
}

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:

if((millis() – tempoAnteriorTarefa1) > PERIODO_TAREFA1)
{
   ExecutaControlePlanta();
   tempoAnteriorTarefa1 = millis();
}

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:

/*
 * @kernel.h
 * 
 * Header file: contém todas as macros, variáveis globais e    
 * protótipos de funções utilizados pelo kernel
 * 
 * Autor: Caio Moraes
 * Data: março/2017
 * Versão: V01
 */
 
#ifndef KERNEL_H
#define KERNEL_H

#include <TimerOne.h>   // Inclui a biblioteca do timer 1

// Definições para as tarefas
#define NUMBER_OF_TASKS  3
#define TEMPO_MAXIMO_EXECUCAO 100   // 100ms

// Definições gerais
#define SUCCESS 1
#define FAIL    0

//------------------------------------------------------------------
// Variáveis do Kernel
//------------------------------------------------------------------
typedef void(*ptrFunc)();  // Definição ponteiro de função

// Definição da estrutura que contem as informações das tarefas
typedef struct{
  ptrFunc Function;
  unsigned char *taskName;
  uint32_t period;
  bool enableTask; 
}TaskHandle;

// Definição do buffer para armazenar as tarefas
TaskHandle* buffer[NUMBER_OF_TASKS]; 

// Variáveis globais do kernel
volatile uint32_t taskCounter[NUMBER_OF_TASKS];
volatile int16_t TempoEmExecucao;
volatile uint32_t sysTickCounter = 0;
volatile bool TemporizadorEstourou;
volatile bool TarefaSendoExecutada;

//------------------------------------------------------------------
// Protótipos de funções do Kernel
//------------------------------------------------------------------
char KernelInit(void);
char KernelAddTask(ptrFunc _function, unsigned char _nameFunction, uint16_t _period, char _enableTask, TaskHandle* task); 
char KernelRemoveTask(TaskHandle* task);
void KernelStart(void);

#endif

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

/* 1) Documentação

      @kernel.ino

   Este código trata-se de um gerenciador de tarefas baseado num    
   kernel cooperativo (não preemptivo.)
   Para isso, utilizou-se um buffer estático para armazenar as   
   tarefas;
   As tarefas são escalonadas de acordo com a interrupção do Timer1.     
   Este verifica o tempo restante para cada tarefa ser executada.
A tarefa que atingir o tempo primeiro, será executada. As "prioridades" das tarefas é por ordem de adição no buffer.

   Autor: Caio Moraes
   Data: março/2017
   Versão: V01
*/

//------------------------------------------------------------------
#include "kernel.h"
#include "avr/wdt.h"

//------------------------------------------------------------------
// Função vKernelInit()
// Descrição: Inicializa as variáveis utilizadas pelo kernel, e o 
//            temporizador responsável pelo tick
// Parâmetros: nenhum
// Saida: nenhuma
//------------------------------------------------------------------
char KernelInit()
{
  memset(buffer, NULL, sizeof(buffer));  // Inicializa o buffer para         
                                         // funções
  memset(taskCounter, 0, sizeof(taskCounter)); // Este vetor armazena o tempo em que as tarefas terminam 

  // Inicializa as variáveis de sinalização do kernel
  TemporizadorEstourou = NAO;
  TarefaSendoExecutada = NAO;

  // Base de tempo para o escalonador
  Timer1.initialize(1000);             // 1ms
  Timer1.attachInterrupt(IsrTimer);    // chama vIsrTimer() quando o            
                                       // timer estoura

  return SUCCESS;
}//end vKernelInit

//------------------------------------------------------------------
// Função KernelAddTask()
// Descrição: Adiciona uma nova Tarefa ao pool
// Parâmetros: funcao da tarefa, nome, periodo, habilita e estrutura //             para armazenar as informações da tarefa
// Saida: nenhuma
//------------------------------------------------------------------
char KernelAddTask(ptrFunc _function, unsigned char _nameFunction, uint16_t _period, char _enableTask, TaskHandle* task)
{
  int i;

  task->Function = _function;
  task->taskName = _nameFunction;
  task->period = _period;
  task->enableTask = _enableTask;
  
  // Verifica se já existe a tarefa no buffer
  for(i = 0; i < NUMBER_OF_TASKS; i++)
  {
    if((buffer[i]!=NULL) && (buffer[i] == task))
       return SUCCESS;
  }

  // Adiciona a tarefa em um slot vazio
  for(i = 0; i < NUMBER_OF_TASKS; i++)
  {
    if(!buffer[i])
    {
      buffer[i] = task;
      return SUCCESS;
    }
  }
  return FAIL;
}//end vKernelAddTask

//------------------------------------------------------------------
// Função KernelRemoveTask()
// Descrição: de forma contrária a função KernelAddTask, esta função //            remove uma Tarefa do buffer circular
// Parâmetros: Nenhum
// Saída: Nenhuma
//------------------------------------------------------------------
char KernelRemoveTask(TaskHandle* task)
{
  int i;
  for(i=0; i<NUMBER_OF_TASKS; i++)
  {
     if(buffer[i] == task)
     {
        buffer[i] = NULL;
        return SUCCESS; 
     }
  }
  return FAIL;

}//end vKernelRemoveTask

//------------------------------------------------------------------
// Função KernelStart()
// Descrição: função responsável por escalonar as tarefas de acordo
//            com a resposta da interrupção do Timer 1
// Parâmetros: Nenhum
// Saída: Nenhuma
//------------------------------------------------------------------void KernelStart()
{
  int i;

  for (;;)
  {
    if (TemporizadorEstourou == SIM)
    {
      for (i = 0; i < NUMBER_OF_TASKS; i++)
      {
        if (buffer[i] != 0)
        {
          if (((sysTickCounter - taskCounter[i])>buffer[i]->period) && (buffer[i]->enableTask == SIM))
          {
            TarefaSendoExecutada = SIM;
            TempoEmExecucao = TEMPO_MAXIMO_EXECUCAO;
            buffer[i]->Function();
            TarefaSendoExecutada = NAO;
            taskCounter[i] = sysTickCounter;
          }
        }
      }
    }
  }
}//end vKernelStart

//------------------------------------------------------------------
// Trata a Interrupção do timer 1
// Decrementa o tempo para executar de cada tarefa
// Se uma tarefa estiver em execução, decrementa o tempo máximo de // execução para reiniciar o MCU caso ocorra algum travamento
//------------------------------------------------------------------void IsrTimer()
{
  int i;
  TemporizadorEstourou = SIM;

  sysTickCounter++;
  
  // Conta o tempo em que uma tarefa está em execução
  if (TarefaSendoExecutada == SIM)
  {
    TempoEmExecucao--;
    if (!TempoEmExecucao)
    {
      // possivelmente a tarefa travou, então, ativa o WDT para reiniciar o micro
      wdt_enable(WDTO_15MS);
      while (1);
    }
  }
}//end vIsrTimer

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:

/* 1) Documentação
 *   
 *    @ESTUDO_RTOS_01.ino
 *    
 *    Este código utiliza o kernel desenvolvido para gerenciar três tarefas:
 *       vDisp7SegTask(): executa um contador de 500ms num display de 7 segmentos. Roda a cada 5ms para
 *                        multiplexar os quatro diplays
 *       vTecladoTask(): faz a leitura de dois pushbuttons. Cada um deles é responsável por
 *                       modificar o sentido da contagem dos contadores.
 *       vDispLcdTask(): roda um contador de 1s num display lcd 16x2
 *    
 *    Autor: Caio Moraes
 *    Data: março/2017
 */ 
//---------------------------------------------------------------------------------------------------------------------
// 2) Diretivas do Pre-processador

#include <LiquidCrystal.h>
#include "kernel.h"
#include "disp7seg.h"

// Definições gerais
#define LED1 13
#define BOTAO1 2
#define BOTAO2 3
#define SIM 1
#define NAO 0
#define TIME_INTERVAL 500   // período para o contador do display de 7 segmentos
#define TASK1_PERIOD 5         // 5ms
#define TASK2_PERIOD 25       // 25ms
#define TASK3_PERIOD 1000   // 1000ms

//---------------------------------------------------------------------------------------------------------------------
// 3) Declarações Globais
boolean botao1FoiPressionado, botao2FoiPressionado;

// Objeto para a biblioteca do display lcd
LiquidCrystal lcd(52,53,51,49,47,45);  // Criando um LCD de 16x2 

//---------------------------------------------------------------------------------------------------------------------
// 4) Protótipos de funções
void vDisp7SegTask();
void vDispLcdTask();
void vTecladoTask();
//---------------------------------------------------------------------------------------------------------------------

// 5) Subrotinas

void setup() 
{
  // Inicializaçãos dos pinos IO
  pinMode(LED1, OUTPUT);
  digitalWrite(LED1, LOW);
  pinMode(BOTAO1, INPUT);
  pinMode(BOTAO2, INPUT);

  // Inicialização da Serial
  Serial.begin(9600);
  
  // Inicialização do módulo LCD
  lcd.begin(16,2); // Inicializando o LCD 16x2
  lcd.clear();          // Limpa o LCD
 
  vDisp7SegInit(); // Inicialização do display de 7 segmentos

  botao1FoiPressionado = NAO;
  botao2FoiPressionado = NAO;

  // Definição das estruturas para armazenarem as informações das tarefas
  TaskHandle task1; 
  TaskHandle task2;
  TaskHandle task3;
  
  KernelInit();                   // Inicializa o kernel
  // Adicionando as tarefas
  KernelAddTask(vDisp7SegTask,    // Função correspondente a tarefa 
                NULL,             // Nome da tarefa (opcional)
                TASK1_PERIOD,     // Periodo da tarefa
                SIM,              // Tarefa habilitada 
                &task1);          // Estrutura da tarefa
  KernelAddTask(vTecladoTask, NULL, TASK2_PERIOD, SIM, &task2);
  KernelAddTask(vDispLcdTask, NULL, TASK3_PERIOD, SIM, &task3);
  KernelStart();                  // Executa o kernel
}

void loop() 
{}

//---------------------------------------------------------------------------------------------------------------------
// vDispLcdTask(): multiplexa no display de 7 segmentos quadruplo, o valor de um contador // de 500ms
//---------------------------------------------------------------------------------------------------------------------
void vDisp7SegTask()
{
  static char disp = 1;
  static uint32_t previousCounter = 0;
  static int16_t valor = 0;
  unsigned char digito;

  if((sysTickCounter - previousCounter) > TIME_INTERVAL)
  {
    previousCounter = sysTickCounter;  // Salva o tempo atual

    // Muda o sentido da contagem 
    if(botao2FoiPressionado == SIM) valor--;
    else valor++;
    // Saturação da contagem
    if(valor < 0) valor = 9999;
    if(valor > 9999) valor = 0;
  }

  // Multiplexação dos displays
  if(disp > 4) disp = 1; 
  digito = ucObtemValorDisplay(valor, disp);
  vEscreveNoDisplay(digito, disp);
  
  disp++;
}//end vDisp7SegTask

//---------------------------------------------------------------------------------------------------------------------
// vDispLcdTask(): escreve no display LCD o valor de um contador sempre que é executada
//---------------------------------------------------------------------------------------------------------------------
void vDispLcdTask(void)
{
  static int count = 0;
  
  // Muda o sentido da contagem 
  if(botao1FoiPressionado == SIM) count--;
  else count++;

  if(count<0) count = 10000;
  if(count > 10000) count = 0;

  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("RTOS STUDY");
  lcd.setCursor(0,1);
  lcd.print(count);
  
}//end vDispLcdTask

//---------------------------------------------------------------------------------------------------------------------
// vTecaldoTask(): Faz a leitura de dois pushbuttons 
//---------------------------------------------------------------------------------------------------------------------
void vTecladoTask(void)
{
  static char flagBotao1 = 0, flagBotao2 = 0;
  
  if(digitalRead(BOTAO1)) flagBotao1 = 1;
  if(!digitalRead(BOTAO1) && flagBotao1){
    botao1FoiPressionado ^= SIM;
    flagBotao1 = 0;
  }

  if(digitalRead(BOTAO2)) flagBotao2 = 2;
  if(!digitalRead(BOTAO2) && flagBotao2){ 
    botao2FoiPressionado ^= SIM;
    flagBotao2 = 0;
  }
}//end vTecladoTask

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.
Comentários:
Notificações
Notificar
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 Mendes 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

Home » Software » Implementando elementos de RTOS no Arduino

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: