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.






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?
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.
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
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!
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.
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!
Muito obrigado Diego.
Valeu pela observação, será corrigido