Apostila de Programação com Arduino

Site: Moodle USP: e-Disciplinas
Curso: PMR3402 - Sistemas Embarcados (2023)
Livro: Apostila de Programação com Arduino
Impresso por: Usuário visitante
Data: terça-feira, 2 jul. 2024, 06:20

1. Entendendo o Programa Blink

Como revisão rápida, vamos analisar um programa super básico de Arduino, adaptado do exemplo Blink, já incluso na instalação do Arduino IDE. Devido à sua simplicidade, pode ser considerado o "Hello World" do Arduino.

Quando executado, o programa abaixo acende o LED embutido no Arduino por um segundo e depois desliga por um segundo, repetidamente. Além disso, a cada vez que o LED é aceso/apagado, uma mensagem é enviada pela porta Serial padrão, cuja saída pode ser observada no IDE do Arduino.

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize serial communication at 9600 bits per second.
  Serial.begin(9600);
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  Serial.println("LED on");          // print out LED switching on
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  Serial.println("LED off");         // print out LED switching off
  delay(1000);                       // wait for a second
}

1.1 Executando no Tinkercad

O Tinkercad é uma aplicação Web gratuita de modelagem tridimensional, bastante popular para criação de modelos para impressão 3D. Ele também possui um módulo para modelagem e simulação de circuitos elétricos, incluindo o Arduino como componente. Esse módulo será utilizado nesta disciplina para demonstrar a programação no Arduino. Note que o mesmo código pode ser executado no Arduino IDE (para quem já possui um Arduino).

Após criar uma conta no site https://www.tinkercad.com/, faça o login e clique em criar um novo circuito. Monte o seguinte circuito:

onde o resistor é de 220Ω.

Em seguida, clique no botão "Código" e selecione Texto em vez de Blocos para ter acesso ao código em C++ do seu software embarcado. Copie e cole o código da subseção anterior, abra o "Monitor Serial" e clique em "▶ Iniciar simulação". Se tudo estiver OK, você deve observar o seguinte:

1.2 Observações sobre o programa Blink

O Arduino suporta codificação do software em C++, que engloba a linguagem C. No entanto, partindo apenas dos conhecimentos de C obtidos nesta disciplina (incluindo a primeira apostila), você deve ter estranhado que o código compila corretamente.

A seguir, são listados algumas explicações para o funcionamento correto da aplicação.

a. Onde está o main?

A falta da função int main(void) é provavelmente a diferença mais saliente de programas Arduino. Na realidade, a função main existe, mas está "escondida"; ela é responsável por chamar as funções setup() e loop(). Para entender isso melhor, vamos ver o código fonte do arquivo main.cpp do ArduinoCore-avr, disponível no GitHub em https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/main.cpp.

...
#include <Arduino.h>

// Declared weak in Arduino.h to allow user redefinitions.
int atexit(void (* /*func*/ )()) { return 0; }

// Weak empty variant initialization function.
// May be redefined by variant files.
void initVariant() __attribute__((weak));
void initVariant() { }

void setupUSB() __attribute__((weak));
void setupUSB() { }

int main(void)
{
	init();

	initVariant();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}

Como pode ser visto, a função setup é chamada uma vez no início do main e a função loop é chamada dentro de um for infinito.

b. Onde estão declaradas e definidas as funções pinMode, digitalWrite e delay? E as constantes simbólicas LED_BUILTIN, OUTPUT, HIGH e LOW?

Observando mais uma vez o main.cpp, vemos que o único arquivo incluído é o Arduino.h. Assim, é de se suspeitar que as declarações estão contidas nesse arquivo.

Realmente, se consultarmos o código em https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/Arduino.h, podemos achar as declarações destas funções e as definições das constantes, exceto o LED_BUILTIN, cuja definição está no arquivo pins_arduino.h.

Para consultar todas as funções e constantes disponíveis na biblioteca padrão do Arduino, acesse a página https://www.arduino.cc/reference/en/

c. O que exatamente é Serial?

Serial é um objeto em C++, é uma instância da classe HardwareSerial (declarada em HardwareSerial.h). Assim, o Serial possui um estado interno (seus atributos) e pode realizar ações através dos seus métodos definidos.

No Arduino, as bibliotecas são geralmente implementadas desse modo, ou seja, como instâncias de classes. Na seção seguinte, mostraremos como criar nossas próprias bibliotecas de Arduino a partir de componentes definidos no diagrama de componentes.

Bibliografia para este capítulo

2. Aplicação Alarme no Arduino

A fim de ilustrar como adaptar a implementação de uma máquina de estados finitos para o Arduino, vamos utilizar o exemplo do nosso alarme implementado em C (https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme). Na sua pasta de sketches (projetos) do Arduino, clone o repositório do alarme para iniciar o desenvolvimento da nossa aplicação.

2.1 Adaptações para projeto Arduino

Antes de começar a editar o código, temos que modificar a estrutura do projeto para adequar ao padrão do Arduino. Essencialmente, necessitamos que o arquivo principal (main.c) tenha o mesmo nome do diretório no qual está contido (alarme) e deve ter a extensão .ino. Para tal, siga os seguintes passos:

  1. mova todos os arquivos da pasta src para a pasta raiz (alarme);
  2. renomeie o arquivo main.c para alarme.ino; e
  3. altere as extensões de todos os arquivos .c para .cpp.

Esta última etapa é necessária pois um sketch Arduino adota a linguagem C++, cuja extensão dos arquivos de código fonte é .cpp.

Por fim, vamos remover os arquivos Makefile, sensores.h, sensores.c, zonas.h e zonas.c, que não serão utilizados. Não deixe de remover as diretivas #include "sensores.h" e #include "zonas.h" do arquivo alarme.ino. Ao final, a estrutura do seu projeto deve ser:

📦alarme
 ┣ 📜.gitignore
 ┣ 📜alarme.ino
 ┣ 📜comunicacao.cpp
 ┣ 📜comunicacao.h
 ┣ 📜definicoes_sistema.h
 ┣ 📜ihm.cpp
 ┣ 📜ihm.h
 ┣ 📜README.md
 ┣ 📜senhas.cpp
 ┣ 📜senhas.h
 ┣ 📜sirene.cpp
 ┣ 📜sirene.h
 ┣ 📜state_machine.svg
 ┣ 📜timer.cpp
 ┗ 📜timer.h

2.2 Substituindo as chamadas de funções da biblioteca padrão de C

No Arduino, não temos as funcionalidades necessárias para realizar as funções printf e scanf, que escreve na tela e lê entradas do teclado, respectivamente. Vamos substituir estas funções pelas de comunicação serial do Arduino.

No caso do printf, basta utilizar o método Serial.print ou Serial.println. No entanto, estes não suportam especificadores de formato. Uma solução é fazer múltiplas chamadas; sendo assim, o código

printf("Estado: %d Evento: %d Acao:%d\n", estado, codigoEvento, codigoAcao);

pode ser substituído por

Serial.print("Estado: ");
Serial.print(estado);
Serial.print(" Evento: ");
Serial.print(codigoEvento);
Serial.print(" Acao: ");
Serial.println(codigoAcao);

Também é necessário substituir os headers stdio.h e stdlib.h por (Arduino.h).

Por exemplo, o arquivo comunicacao.cpp deve ficar:

#include <Arduino.h>

#include "definicoes_sistema.h"
#include "comunicacao.h"

/************************
 com_notificar
 Envia mensagem para a Central
 entradas
   texto : texto para envio para Central
 saidas
   nenhuma
*************************/
void com_notificar(char* texto)
{
    Serial.print("Comunicacao com a Central: ");
    Serial.println(texto);
}

Por fim, é necessário inicializar a comunicação serial na função setup() do alarme.ino, como no exemplo do capítulo 1.

2.3 Adaptação dos componentes ihm e Timer

Os eventos serão gerados a partir do recebimento de mensagens via serial. Assim, a função ihm_obterTeclas do componente ihm pode ser modificado para

...

/************************
 ihm_obterTecla
 Obtem tecla do teclado
 entradas
   nenhuma
 saidas
   teclas lidas do teclado
*************************/
char* ihm_obterTeclas()
{
  // Serial.print("obter teclas:");
  int read_count = 0;
  
  // check for input
  if (Serial.available() > 0) {
    // read the incoming bytes:
    read_count = Serial.readBytesUntil('\n',buf, sizeof(buf)/sizeof(buf[0]) - 1);
  }
  
  buf[read_count] = '\0';
  if(read_count > 0) {
  	Serial.println(buf);
  }
  
  return buf;
}
}

Vamos implementar o componente timer utilizando o relógio do próprio Arduino. Assim, o arquivo timer.cpp deve ser modificado para

#include <Arduino.h>

#include "definicoes_sistema.h"
#include "timer.h"

#define TEMPO 10

int tmr_situacao = false;
unsigned long horaInicio;

/*******************************
 tmr_iniciar
 Aciona ou desaciona o timer
 entradas
   controle: TRUE:liga FALSE:desliga
 saidas
   nenhuma
********************************/
void tmr_iniciar(int controle)
{
   tmr_situacao = controle;
   if (controle)
   {
      horaInicio = millis();
   }
}

/*******************************
 tmr_timeout
 Retorna se o timer esta em timeout.
 entradas
    nenhuma
 saidas
    FALSE: nao houve estouro do temporizador
    TRUE: houve estouro do temporizador
********************************/
int tmr_timeout()
{
    if (tmr_situacao == false)
    {
        return false;
    }
    if(millis() - horaInicio > TEMPO*1000)
    {
        return true;
    }
    return false;
}

2.4 Reestruturação do código para setup e loop

Agora vamos trabalhar no arquivo principal, o alarme.ino. Analisando a função main, podemos separá-la em inicialização e operação, que ocorre dentro de um loop while infinito.

Inicialmente, vamos mover a inicialização das variáveis simples para o escopo global, ou seja, fora da função main, escreva

...
/***********************************************************************
 Estaticos
 ***********************************************************************/
  int codigoEvento = NENHUM_EVENTO;
  int eventoInterno = NENHUM_EVENTO;
  int estado = ESPERA;
...

Assim, a nossa função setup deve ficar:

void setup() {
  Serial.begin(9600);

  iniciaSistema();
  Serial.println("Alarme iniciado");
} // setup

Depois, devemos mover o loop infinito para a função loop:

void loop() {
  if (eventoInterno == NENHUM_EVENTO) {
      codigoEvento = obterEvento();
  } else {
      codigoEvento = eventoInterno;
  }
  if (codigoEvento != NENHUM_EVENTO)
  {
      codigoAcao = obterAcao(estado, codigoEvento);
      estado = obterProximoEstado(estado, codigoEvento);
      eventoInterno = executarAcao(codigoAcao);
      Serial.print("Estado: ");
      Serial.print(estado);
      Serial.print(" Evento: ");
      Serial.print(codigoEvento);
      Serial.print(" Acao: ");
      Serial.println(codigoAcao);
  }
} // loop

Com isso, finalizamos a nossa aplicação de alarme. Para testar, compile e faça o upload para o Arduino; sua operação pode ser realizada totalmente pelo monitor serial do Arduino IDE.

No caso de dúvidas, o código final da aplicação pode ser consultado em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_basico

2.5 Código final completo no Tinkercad

O Tinkercad possui a limitação de apenas um arquivo de código fonte. Isto significa que temos que juntar todos os arquivos do projeto do alarme.

No repositório foi incluído um script Python que faz a conversão automática dos projetos de alarme:

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/blob/master/ino2Tinkercad.py

para usá-lo, é necessário executar o seguinte comando a partir da pasta do projeto:

python ..\ino2Tinkercad.py alarme.ino -o ../alarme_tinkercad.ino

Depois copie e cole o conteúdo de alarme_tinkercad.ino para o seu circuito no Tinkercad.

O código final pode ser obtido em:

https://gitlab.uspdigital.usp.br/-/snippets/17

2.6 Adicionando um buzzer piezoelétrico

Para deixar nossa aplicação um pouco mais interessante, vamos adicionar um buzzer para simular a sirene do nosso alarme. Primeiro, crie o seguinte circuito:

Depois, declare uma nova função do componente sirene em sirene.h:

...

/************************
 sne_setup
 Configura a sirene
 entradas
   pin: pino do buzzer
 saidas
   nenhuma
*************************/
extern void sne_setup(int pin);

..

e modifique as suas definições em sirene.cpp:

#include <Arduino.h>

#include "definicoes_sistema.h"
#include "sirene.h"

#define TONE_FREQ 1000
#define BEEP_TIME 100

int sne_pin;

/************************
 sne_setup
 Configura a sirene
 entradas
   pin: pino do buzzer
 saidas
   nenhuma
*************************/
void sne_setup(int pin)
{
  sne_pin = pin;
  pinMode(sne_pin, OUTPUT);
}

/************************
 sne_bip
 Aciona momentaneamente a sirene
 entradas
   nenhuma
 saidas
   nenhuma
*************************/
void sne_bip()
{
  tone(sne_pin, TONE_FREQ, BEEP_TIME);
}


/************************
 sne_acionar
 Aciona ou desaciona a sirene
 entradas
   controle: TRUE:ligar FALSE:desligar
 saidas
   nenhuma
*************************/
void sne_acionar(int controle)
{
  if(controle)
    tone(sne_pin, TONE_FREQ);
  else
    noTone(sne_pin);
}

Por fim, no arquivo .ino principal, insira a chamada para a função sne_setup() dentro de setup:

void setup()
{
  sne_setup();
  Serial.begin(9600);
  Serial.println("Maquina de estados iniciada");
}

Agora, ao acionar o alarme, o buzzer deve gerar um beep curto; e quando o alarme for disparado, o buzzer deve emitir um som contínuo.

Bibliografia para este capítulo

3. Aprimorando a aplicação Alarme com C++ (namespaces)

Nosso programa está estruturado em arquivos de acordo com a nossa separação em componentes. No entanto, mesmo com essa divisão, as funções e variáveis estão todas declaradas em escopo global. Isso implica que não podemos ter duas funções ou variáveis com o mesmo nome e, como possuem acesso global, é passível de confusão entre componentes.

A solução utilizada foi adotar um prefixo diferente para cada componente. Isso deve ser feito de forma manual pelo programador para cada identificador (função ou variável) de cada componente, criando muitas situações de possível erro.

3.1 Namespaces

Em C++, podemos contornar esse problema com o uso de namespaces, delimitando uma região do código que providencia um escopo próprio para os identificadores.

É bastante simples criar um namespace, como podemos ver no trecho definido no escopo global:

namespace first_space {
  int val = 10;
  void foo() {
    printf("Inside first_space\n");
  }
  void bar() {
    printf("val = %d\n", val);
  }
}

namespace second_space {
  void foo() {
    printf("Inside second_space\n");
  }
}

onde foram criados dois namespaces. Note que, como cada um delimita um escopo diferente, é possível declarar duas funções com o mesmo nome (foo), mas com implementações distintas.

Para acessar os identificadores dos namespaces criados, podemos usar o seguinte código:

int main()
{
    first_space::foo();
    second_space::foo();
    first_space::val = 20;
    first_space::bar();

    return 0;
}

cuja saída deve ser

Inside first_space
Inside second_space
val = 20

Se, dentro de um arquivo .cpp, acessamos muitas vezes funções de um mesmo namespace, podemos omitir o nome do namespace usando a sintaxe using namespace. Assim, no nosso exemplo, obteríamos o mesmo resultado escrevendo:

using namespace first_space;

int main()
{
    foo();
    second_space::foo();
    val = 20;
    bar();

    return 0;
}

3.2 Utilizando namespaces no componente Timer

Para nossa aplicação "alarme", como desejamos isolar os identificadores de cada componente, podemos criar namespaces para cada um deles. Dessa maneira, podemos reescrever o arquivo timer.h para:

#ifndef TIMER_H_INCLUDED
#define TIMER_H_INCLUDED

namespace Timer {
   extern void iniciar(int controle);
   extern int timeout();
}

#endif // TIMER_H_INCLUDED

e o arquivo timer.cpp para:

#include <Arduino.h>

#include "definicoes_sistema.h"
#include "timer.h"

#define TEMPO 10

namespace Timer {
   int situacao = false;
   unsigned long horaInicio;
}

void Timer::iniciar(int controle)
{
   situacao = controle;
   if (controle)
   {
      horaInicio = millis();
   }
}

int Timer::timeout()
{
    if (situacao == false)
    {
        return false;
    }
    if(millis() - horaInicio > TEMPO*1000)
    {
        return true;
    }
    return false;
}

Assim removemos a necessidade de usar o prefixo tmr para todas as funções e variáveis. Note que poderíamos declarar as variáveis situacao e horaInicio no header com o modificador extern caso necessitássemos que pudessem ser acessadas em outros arquivos.

Para usar o componente no arquivo principal alarme.ino, temos que modificar o acesso às funções do componente Timer. Assim, a função executarAcao fica:

int executarAcao(int codigoAcao)
{
    int retval;

    retval = NENHUM_EVENTO;
    if (codigoAcao == NENHUMA_ACAO)
        return retval;

    switch(codigoAcao)
    {
    case A01:
        Timer::iniciar(true);
        break;
    case A02:
        sne_bip();
        com_notificar("Alarme em alerta");
        Timer::iniciar(false);
        break;
    case A03:
        com_notificar("Alarme desacionado");
        Timer::iniciar(false);
        break;
    case A04:
        com_notificar("Alarme desacionado");
        break;
    case A05:
        Timer::iniciar(true);
        break;
    case A06:
        sne_acionar(true);
        com_notificar("Invasao");
        Timer::iniciar(false);
        break;
    case A07:
        com_notificar("Alarme desacionado");
        Timer::iniciar(false);
        sne_acionar(false);
        break;
    } // switch

    return retval;
} // executarAcao

Para finalizar o aprimoramento do código, idealmente todos os componentes devem possuir o seu próprio namespace. Isto fica como exercício.

No caso de dúvidas, o código final da aplicação pode ser consultado em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_namespaces

Bibliografia para este capítulo

4. Aprimorando a aplicação Alarme com C++ (classes)

Um modo mais tradicional de implementar o diagrama de componentes UML é através de classes. Na nossa implementação original, isso não era possível, pois C não possui suporte à programação orientada a objetos (POO). Já no caso da programação no Arduino, que é feita em C++, podemos utilizar classes e objetos, nos beneficiando das propriedades clássicas de POO como encapsulamento, herança e polimorfismo.

4.1 Diagrama de componentes do alarme

Antes de iniciar a conversão, vamos revisar o diagrama de componentes:

Diagrama de componentes

4.2 Criando a classe Timer

Vamos nos concentrar no componente Timer, que possui duas portas: iniciar e timeout. Estes são os nossos dois métodos públicos, que podem ser acessados em qualquer escopo. Assim, podemos declarar a classe Timer e seus métodos no arquivo timer.h

#ifndef TIMER_H_INCLUDED
#define TIMER_H_INCLUDED

class Timer {
  public:
  Timer();
  void iniciar(int controle);
  int timeout();

  private:
  int situacao;
  unsigned long horaInicio;
};

#endif // TIMER_H_INCLUDED

onde adicionamos dois membros privados,situação e horaInicio, e o construtor. Os dois membros estão encapsulados, uma vez que só podem ser acessados dentro dos métodos da instância.

Note que é recomendado utilizar o padrão de iniciar o nome da classe com letra maiúscula.

As definições podem ser adicionadas no arquivo timer.cpp

#include <Arduino.h>

#include "definicoes_sistema.h"
#include "timer.h"

#define TEMPO 10

Timer::Timer()
{
   situacao = false;
}

void Timer::iniciar(int controle)
{
   situacao = controle;
   if (controle)
   {
      horaInicio = millis();
   }
}

int Timer::timeout()
{
    if (situacao == false)
    {
        return false;
    }
    if(millis() - horaInicio > TEMPO*1000)
    {
        return true;
    }
    return false;
}

onde Arduino.h foi incluído para poder utilizar as função millis.

4.3 Lista de inicialização de membros (opcional)

Observe que a variável situacao foi inicializada dentro do corpo do construtor. Neste caso, a variável é primeiramente inicializada sem valor definido e depois o seu valor é modificado com o operador de atribuição. Ou seja, é algo similar a

int situacao;

int main() {
  situacao = false;
  ...

O que é ineficiente, pois estamos atribuindo valores duas vezes para a variável. O ideal seria escrever

int situacao = false;

Para membros de classes, um comportamento parecido com o código acima pode ser obtido utilizando a lista de inicialização de membros. No caso do componente Timer, isso significa que a definição do construtor deve ser mudada para:

Timer::Timer() : situacao(false)
{
}

Note que, para membros com modificador const ou para usar a sintaxe inicialização de array, é obrigatório o uso da lista de inicialização de membros.

4.4 Utilizando o componente Timer

Para utilizar a nova implementação do componente, vamos criar uma instância da classe no arquivo principal (p. ex. alarme.ino)

#include "definicoes_sistema.h"
#include "comunicacao.h"
#include "ihm.h"
#include "senhas.h"
#include "sirene.h"
#include "timer.h"

Timer tmr;
...

Depois, é necessário modificar a função executarAcao()

int executarAcao(int codigoAcao)
{
    int retval;

    retval = NENHUM_EVENTO;
    if (codigoAcao == NENHUMA_ACAO)
        return retval;

    switch(codigoAcao)
    {
    case A01:
        tmr.iniciar(true);
        break;
    case A02:
        sne_bip();
        com_notificar("Alarme em alerta");
        tmr.iniciar(false);
        break;
    case A03:
        com_notificar("Alarme desacionado");
        tmr.iniciar(false);
        break;
    case A04:
        com_notificar("Alarme desacionado");
        break;
    case A05:
        tmr.iniciar(true);
        break;
    case A06:
        sne_acionar(true);
        com_notificar("Invasao");
        tmr.iniciar(false);
        break;
    case A07:
        com_notificar("Alarme desacionado");
        tmr.iniciar(false);
        sne_acionar(false);
        break;
    } // switch

    return retval;
} // executarAcao

Para finalizar o aprimoramento do código, idealmente todos os componentes devem ser convertidos para classes. Isto fica como exercício.

No caso de dúvidas, o código final da aplicação pode ser consultado em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_classes

4.5 Adaptando para o padrão de biblioteca do Arduino (opcional)

As bibliotecas do Arduino, como a Serial, são definidas utilizando classes. No entanto, para utilizá-las, às vezes é necessário instanciar objetos, principalmente se não existem argumentos no construtor. Isso porque, ao incluir o header, já é instanciado um único objeto, que é utilizado pelo usuário para acessar os métodos da biblioteca.

Vamos transformar o componente Timer em uma biblioteca do Arduino. Para isso, precisamos primeiro declarar a instância no escopo global no arquivo timer.h

#ifndef TIMER_H_INCLUDED
#define TIMER_H_INCLUDED

class Timer {
  public:
  Timer();
  void iniciar(int controle);
  int timeout();

  private:
  int situacao;
  unsigned long horaInicio;
};

extern Timer tmr;

#endif // TIMER_H_INCLUDED

Já no arquivo timer.cpp é necessário adicionar a linha

Timer tmr;

para definição da variável global tmr.

Por fim, no arquivo principal do projeto (alarme.ino), remova a linha

Timer tmr;

Note que, para simular uma biblioteca, o nome da instância deve iniciar com letra maiúscula. No nosso caso, o mais apropriado seria utilizar o nome Timer para a instância. Para tanto, deveríamos modificar o nome da classe para não coincidir o do objeto. Isto fica como exercício, assim como a adaptação dos outros componentes para bibliotecas Arduino.

Bibliografia para este capítulo

5. Aprimorando a aplicação Alarme com C++ (interfaces ou classes abstratas)

A divisão do sistema em componentes pode ser feita de diversas maneiras. Uma forma de se pensar em componentes é definir quais partes do sistema podem ser compradas e/ou atualizadas independentemente.

Isto implica que o nosso código principal deve funcionar praticamente do mesmo modo no caso de troca de componentes. Este comportamento pode ser obtido utilizando o conceito de interfaces, também conhecido como classe abstrata em C++.

5.1 Herança em C++

Para criar uma subclasse em C++, utilizamos a sintaxe

class subclasse: especificador-acesso superclasse

No geral, especificador-acesso é public, o que significa que mantemos o mesmo controle de acesso da classe pai.

No exemplo

class Fruit {
  public:
    Fruit() {};
    void setColor(char *_color) { color = _color; }
    char *getColor() { return color; }	
  protected: 
    char *color;
  private: 
    double acidity;
};

class Banana: public Fruit {
  public:
    Banana() {};
}

criamos uma classe Banana que é subclasse de Fruit. Dentro da definição dos métodos de Banana, podemos acessar apenas os identificadores públicos e protegidos da superclasse. Isso significa que não temos acesso ao membro acidity, pois este é privado.

Como é tradicional em orientação a objetos, a classe filha herda os métodos e membros da classe pai.

5.2 Métodos virtuais puros e classes abstratas

Em C++, podemos declarar métodos virtuais puros, que não possuem definição. Estes geralmente são declarados nas classes bases, de modo que devem ser redefinidos nas classes filhas.

Vamos utilizar este conceito na nossa classe Fruit:

class Fruit {
  public:
    Fruit();
    virtual void smell() = 0;
    void setColor(char *_color) { color = _color; }
    char *getColor() { return color; }	
  protected: 
    char *color;
  private: 
    double acidity;
};

Note que não existe definição do método smell e, assim, NÃO é possível instanciar objetos da classe Fruit. Isto é, a seguinte linha ocorre em erro:

Fruit alguma_fruta; // Erro!

No entanto, podemos criar uma subclasse e instanciá-la, desde que implementemos todos os método virtuais da superclasse:

class Banana: public Fruit {
  public:
    Banana() {};
    void smell() {
      printf("Smells like banana spirit\n");
    }
}

Banana alguma_banana; // OK

Classes abstratas são classes que possuem pelo menos um método virtual puro e, consequentemente, não podem ser instanciadas. Estas podem ser utilizadas para implementar interfaces, como veremos a seguir.

5.3 Novo diagrama de componentes

Com isso em mente, vamos propor um novo diagrama de componentes que implementa interfaces para cada componente do nosso sistema.

Diagrama de componentes

Para implementação das interfaces, utilizaremos classes abstratas com métodos virtuais puros públicos. Estes servirão de classe base para as subclasses que implementam os componentes.

Aqui mantivemos o nome original dos nossos componentes para a interfaces, de modo a diminuir o impacto no código original. Assim, tivemos que modificar o nome de todos os componentes. Outra forma de definir nomes interfaces é anexar Interface no fim, podendo, assim, manter o nome original dos componentes. Nesse caso, por exemplo, a interface do Timer se chamaria TimerInterface.

5.4 Criando a interface Timer e a classe TimerInterno

Para a interface Timer, precisamos converter a classe original para uma classe abstrata. No arquivo timer.h, deixamos apenas métodos, que devem ser convertidos para virtuais:

#ifndef TIMER_H_INCLUDED
#define TIMER_H_INCLUDED

class Timer {
  public:
  virtual void iniciar(int controle) = 0;
  virtual int timeout() = 0;
};

#endif // TIMER_H_INCLUDED

Como são métodos virtuais, não possuem definição e, desse modo, podemos deletar o arquivo timer.cpp.

Para criar o componente TimerInterno, vamos criar uma subclasse de Timer. Ou seja, no arquivo timer_interno.h, temos:

#ifndef TIMER_INTERNO_H_INCLUDED
#define TIMER_INTERNO_H_INCLUDED

#include "timer.h"

class TimerInterno: public Timer {
  public:
  TimerInterno();
  void iniciar(int controle);
  int timeout();

  private:
  int situacao;
  unsigned long horaInicio;
};

#endif // TIMER_INTERNO_H_INCLUDED

Para o arquivo timer_interno.cpp, considere que as definições dos métodos e inicialização dos membros é idêntica ao da classe Timer original.

Para finalizar o aprimoramento do código, idealmente devemos implementar todos os componentes com interface, de acordo com o novo diagrama. Isto fica como exercício.

No caso de dúvidas, o código final da aplicação pode ser consultado em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_interfaces

Bibliografia para este capítulo

6. Programando com tarefas: aplicação Alarme com escalonamento estático periódico

A programação com tarefas consiste em separar o código em blocos de processamento com diferentes funcionalidades. Esta é uma abordagem bastante utilizada para programar sistema de tempo real, pois permite estabelecer requisitos temporais às tarefas. Em sistemas operacionais de tempo real (Real Time Operating Systems ou RTOS), tais restrições são impostas por um algoritmo de escalonamento.

Mesmo em sistemas sem requisitos temporais críticos, a estruturação com tarefas pode melhorar a organização do código e facilitar a identificação de bugs. Este último é especialmente importante no caso da existência de rotinas com periodicidade e/ou processamento de interrupções, pois ambas possuem características temporais importantes.

6.1 Programação com "super loop"

Vamos analisar a nossa aplicação Alarme para identificar possíveis tarefas. No geral, os programas de Arduino utilizam a estrutura "super loop", que significa que todas as tarefas são processadas dentro da função loop(), como mostrado na Fig. abaixo.

Cada uma das tarefas listadas como exemplos são responsáveis por funcionalidades distintas do sistema. À parte de alguns pontos de sincronização, elas poderiam ser executadas em paralelo. No entanto, isso não é sempre possível em sistemas embarcados, além de introduzir mais complexidade no código.

6.2 Divisão em tarefas

Na realidade, a separação entre tarefas não é tão clara como o exemplo da Fig. anterior. Se considerarmos duas tarefas, por exemplo:

  1. processamento da máquina de estados; e
  2. obtenção dos eventos (entrada da porta serial timeout);

notamos que cada uma está em um local bastante diferente do código.

Para separar as tasks, vamos criar novas funções para cada uma. A máquina de estados pode ser escrita na função taskMaqEstados(), com o código:

void taskMaqEstados() {
  for(;;) {
    if (eventoInterno != NENHUM_EVENTO) {
        codigoEvento = eventoInterno;
    }
    if (codigoEvento != NENHUM_EVENTO)
    {
        codigoAcao = obterAcao(estado, codigoEvento);
        estado = obterProximoEstado(estado, codigoEvento);
        eventoInterno = executarAcao(codigoAcao);
        Serial.print("Estado: ");
        Serial.print(estado);
        Serial.print(" Evento: ");
        Serial.print(codigoEvento);
        Serial.print(" Acao: ");
        Serial.println(codigoAcao);
    }
  }
}

onde removemos a instrução codigoEvento = obterEvento();.

Já a obtenção de eventos pode ser implementada com a função taskObterEvento, com:

void taskObterEvento() {
  for(;;) {
    codigoEvento = NENHUM_EVENTO;

    teclas = ihm.obterTeclas();
    if (decodificarAcionar()) {
      codigoEvento = ACIONAR;
      return;
    }
    if (decodificarDesacionar()) {
      codigoEvento = DESACIONAR;
      return;
    }
    if (decodificarTimeout()) {
      codigoEvento = TIMEOUT;
      return;
    }
    if (decodificarDisparar()) {
      codigoEvento = DISPARAR;
      return;
    }
  }
}

Se as duas tarefas pudessem ser executadas em paralelo, a aplicação poderia funcionar de modo similar à original. Porém, como esse não é sempre o caso, temos que distribuir a execução de cada tarefa de modo a "simular" este paralelismo.

Uma forma simples de se solucionar esse problema é transformar cada função em um tarefa periódica, isto é, que executa a cada X milissegundos, como mostrado na Fig.:

Se a periodicidade for alta, ou seja, o intervalo entre execuções for muito curto, a alternância de tarefas é muito rápida e, assim, aparenta estar executando concorrentemente.

6.3 Implementando um algoritmo de escalonamento periódico: configurando a interrupção tick

Em geral, algoritmos de escalonamento dividem o tempo em intervalos discretos, chamados de ticks. Assim, todos os requisitos devem ser reescritos em unidades de ticks, que devem ser pequenos, uma vez que delimitam a resolução do nosso escalonamento. Vamos implementar a contagem de ticks utilizando uma interrupção gerada pelo timer do Arduino:

void setupTickInterrupt(long uSecs) {
  noInterrupts();           // disable all interrupts

  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  // compare match register 16MHz/256 * t(s) - 1
  OCR1A = (16e6 / 256L * uSecs) / 1e6 - 1;            

  TCCR1B |= (1 << WGM12);   // CTC mode
  TCCR1B |= (1 << CS12);    // 256 prescaler 
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt

  interrupts();             // enable all interrupts
}

Como pode ser visto, a configuração da interrupção é feita manipulando os bits do registrador, cuja explicação está fora do escopo desta disciplina. O importante é compreender que, uma vez chamada a partir do setup(), esta função vai configurar uma interrupção que ocorre a cada uSecs microssegundos; este será o intervalo do nosso tick.

6.4 Implementando um algoritmo de escalonamento periódico: criando a estrutura de dados do descritor de tarefa

Vamos implementar o nosso escalonador na classe TaskSwitcher, que será instanciada como uma biblioteca de Arduino. As declarações da classe e do struct auxiliar estão incluídas no arquivo task_switcher.h:

#ifndef TASK_SWITCHER_H_INCLUDED
#define TASK_SWITCHER_H_INCLUDED

#define MAX_TASKS 3

typedef struct {
void (*task)();
long     interval;
long     current_time;
int      status;
} TaskControl;

class TaskSwitcher {
    public:
    TaskSwitcher();
    void begin(long timerInterruptInuSecs);
    void createTask(void (*t)(), long interval);
    void runCurrentTask();
    void updateTickCounter();

    private:
    TaskControl taskList[MAX_TASKS];
    int taskCount;
};

extern TaskSwitcher TaskController;

#endif // TASK_SWITCHER_H_INCLUDED

Note que a função begin contém o código de configuração de interrupções de tick descrita na seção anterior. [comment]: # (Contém o código ou é a própria função definida anteriormente? Não está muito claro.)

Quanto ao struct TaskControl, este é o descritor usado para armazenar os dados da tarefa, que contém:

  • um ponteiro para a função (p. ex.: taskObterEvento());
  • o intervalo de execução da tarefa(período);
  • e duas variáveis para controle do escalonador (status e current_time).

Por fim, o membro taskList armazena os descritores de todas as tarefas, que são criados e adicionados com o método createTask.

6.5 Implementando um algoritmo de escalonamento periódico: processamento da interrupção

Para entender o funcionamento do escalonador, vamos observar o que ocorre dentro da função da interrupção:

TaskSwitcher TaskController;

ISR(TIMER1_COMPA_vect) {
   TaskController.updateTickCounter();
}

void TaskSwitcher::updateTickCounter() {
  int i;
  for (i=0; i< taskCount; i++) {
    if (taskList[i].status == WAIT) {
      taskList[i].current_time++;
      if (taskList[i].current_time >= taskList[i].interval) {
        taskList[i].status = READY;
      }
    } // if task is WAITing
  } // for each task
}

Basicamente, para cada tarefa, o valor de current_time é incrementado e o status da tarefa pode ser atualizado (de WAIT para READY).

E também vamos investigar a função runCurrentTask(), que deve ser chamada no laço infinito do programa Arduino:

void TaskSwitcher::runCurrentTask() {
  int i;
  void (*task)();
  for (i=0; i<  taskCount; i++) {
    if (taskList[i].status == READY) {
      noInterrupts();
      taskList[i].status = WAIT;
      taskList[i].current_time = 0;
      interrupts();
      task = taskList[i].task;
      (*task)();
    } // if task is READY
  } //for each task
}

Aqui cada tarefa em READY é executada (na instrução (*task)();) e o seu estado é atualizado para WAIT.

Explicação do funcionamento do TaskSwitcher:

Cada tarefa implementa a seguinte máquina de estados:

O current_time mede o tempo (em unidades de tick) desde a última execução (transição de READY para WAIT). Assim, quando é detectado que passou o tempo do intervalo da tarefa, o seu estado é modificado para pronta para execução (READY). A execução de fato da tarefa é feita na função runCurrentTask(), que, ao agendar tarefa, a move novamente para o estado de espera (WAIT).

6.6 Implementado um algoritmo de escalonamento periódico: aplicação Alarme

Para utilizar a biblioteca TaskSwitcher, precisamos modificar apenas o arquivo alarme.ino.

Após incluir o header task_switcher.h, vamos criar as funções da tarefa:

void taskMaqEstados() {
  if (eventoInterno != NENHUM_EVENTO) {
      codigoEvento = eventoInterno;
  }
  if (codigoEvento != NENHUM_EVENTO)
  {
      codigoAcao = obterAcao(estado, codigoEvento);
      estado = obterProximoEstado(estado, codigoEvento);
      eventoInterno = executarAcao(codigoAcao);
      Serial.print("Estado: ");
      Serial.print(estado);
      Serial.print(" Evento: ");
      Serial.print(codigoEvento);
      Serial.print(" Acao: ");
      Serial.println(codigoAcao);
  }


void taskObterEvento() {
  codigoEvento = NENHUM_EVENTO;

  teclas = ihm.obterTeclas();
  if (decodificarAcionar()) {
    codigoEvento = ACIONAR;
    return;
  }
  if (decodificarDesacionar()) {
    codigoEvento = DESACIONAR;
    return;
  }
  if (decodificarTimeout()) {
    codigoEvento = TIMEOUT;
    return;
  }
  if (decodificarDisparar()) {
    codigoEvento = DISPARAR;
    return;
  }

Note que, diferente das funções descritas na seção 6.2, as tarefas não são implementadas em loop infinito. Elas foram adaptadas para se adequarem ao nosso algoritmo de escalonamento, que não tem preempção, ou seja, não consegue interromper a execução de uma tarefa.

O próximo passo é criar as tarefas na função setup():

void setup() {
  Serial.begin(9600);

  // configure tasks
  TaskController.createTask(&taskMaqEstados, 500);
  TaskController.createTask(&taskObterEvento, 500);
  
  // set up timer interrupt 
  TaskController.begin(1000); // tick @1ms (1000 us)

  iniciaSistema();
  Serial.println("Alarme iniciado");
} // setup

onde foram criadas duas tarefas periódicas com intervalo de 500ms (500 * 1000 microssegundos).

Agora só falta chamar o métodorunCurrentTask a partir da função loop():

void loop() {
  TaskController.runCurrentTask();
} // loop

Ao executar, o comportamento temporal do processamento das tasks deve seguir de forma aproximada o padrão da Fig.:

No caso de dúvidas, o código final da aplicação pode ser consultado em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_taskswitcher

Bibliografia para este capítulo

7. FreeRTOS: Instalação e Programa Hello World

No limite, o TaskSwitcher desenvolvido na seção anterior pode ser considerado um sistema operacional extremamente simples. Afinal, o escalonamento de tarefas é um dos componentes importantes de um sistema operacional. No caso de sistemas operacionais de tempo real (RTOS), o algoritmo de escalonamento é o principal, uma vez que o seu objetivo primordial é garantir os requisitos temporais na execução das tarefas.

Comparado com o TaskSwitcher, no entanto, os RTOSes possuem algoritmos muito mais complexos, além de outras funcionalidades. Nesta disciplina, utilizaremos o FreeRTOS, que possui código aberto e é uma alternativa bastante popular.

7.1 Instalação do FreeRTOS

No Arduino, a instalação do FreeRTOS é muito fácil, podendo ser obtido diretamente no repositório de biblioteca.

Infelizmente, o Tinkercad suporta apenas uma quantidade limitada de bibliotecas, o que não inclui o FreeRTOS. Desse modo, para realizarmos o estudo do FreeRTOS, vamos utilizar o port do FreeRTOS para desktop (Windows, Linux e MacOS). Nesta apostila, focaremos na versão Windows com MinGW, mas o procedimento para os outros SOs não é muito diferente.

Sendo assim, antes de iniciar a instalação, verifique se o MinGW está instalado. Caso não esteja, siga os passos da apostila de C. Outra opção para executar os códigos deste e dos próximos capítulos é rodar no Google Colab a partir do notebook disponibilizado na página da disciplina. Note que o processo de instalação no Linux/MacOS é semelhante ao empregado no Google Colab.

7.2 Obtendo o código fonte

O modo padrão de utilização do FreeRTOS é incluir os arquivos fonte (.c e .h) diretamente no projeto da nossa aplicação. Então, o primeiro passo consiste em obter o código fonte do FreeRTOS, que é disponibilizado no repositório:

https://github.com/FreeRTOS/FreeRTOS

Para não baixar todos os submódulos (o que implicaria em quase 1GB de código), vamos clonar apenas o kernel, que basicamente é o programa núcleo de um sistema operacional:

$ git clone https://github.com/FreeRTOS/FreeRTOS-Kernel.git

7.3 Criando um projeto novo: copiando os arquivos do código fonte

Crie uma pasta nova para o seu projeto novo, que pode chamar freertos-tutorial com o comando

$ mkdir freertos-tutorial

Agora vamos copiar os arquivos .c necessários com o comando:

$ cp ./FreeRTOS-Kernel/tasks.c ./freertos-tutorial
$ cp ./FreeRTOS-Kernel/queue.c ./freertos-tutorial
$ cp ./FreeRTOS-Kernel/list.c ./freertos-tutorial
$ cp ./FreeRTOS-Kernel/timers.c ./freertos-tutorial
$ cp ./FreeRTOS-Kernel/event_groups.c ./freertos-tutorial
$ cp ./FreeRTOS-Kernel/portable/MSVC-MingW/*.c ./freertos-tutorial
$ cp ./FreeRTOS-Kernel/portable/MemMang/heap_4.c ./freertos-tutorial

Agora vamos copiar os headers:

$ mkdir freertos-tutorial/include
$ cp ./FreeRTOS-Kernel/include/*.* ./freertos-tutorial/include
$ cp ./FreeRTOS-Kernel/portable/MSVC-MingW/*.h ./freertos-tutorial

Se preferir, pode copiar manualmente os arquivos ao invés de utilizar os comandos acima. No final do processo, a sua estrutura de diretórios deve ser:

📦freertos-tutorial
 ┣ 📂include
 ┃ ┣ 📜atomic.h
 ┃ ┣ 📜croutine.h
 ┃ ┣ 📜deprecated_definitions.h
 ┃ ┣ 📜event_groups.h
 ┃ ┣ 📜FreeRTOS.h
 ┃ ┣ 📜list.h
 ┃ ┣ 📜message_buffer.h
 ┃ ┣ 📜mpu_prototypes.h
 ┃ ┣ 📜mpu_wrappers.h
 ┃ ┣ 📜portable.h
 ┃ ┣ 📜projdefs.h
 ┃ ┣ 📜queue.h
 ┃ ┣ 📜semphr.h
 ┃ ┣ 📜StackMacros.h
 ┃ ┣ 📜stack_macros.h
 ┃ ┣ 📜stdint.readme
 ┃ ┣ 📜stream_buffer.h
 ┃ ┣ 📜task.h
 ┃ ┗ 📜timers.h
 ┣ 📜event_groups.c
 ┣ 📜heap_4.c
 ┣ 📜list.c
 ┣ 📜port.c
 ┣ 📜portmacro.h
 ┣ 📜queue.c
 ┣ 📜tasks.c
 ┗ 📜timers.c

O procedimento acima expande as instruções dadas no tutorial do FreeRTOS disponível no site oficial em

https://www.freertos.org/Documentation/RTOS_book.html

7.4 Criando um projeto novo: criando o arquivo de configuração FreeRTOSConfig.h

O arquivo FreeRTOSConfig.h permite personalizar o FreeRTOS para sua aplicação, por meio de várias configurações, incluindo as do escalonador. Para os exercícios desta apostila, uma configuração padrão pode ser utilizada.

Crie o arquivo freertos-tutorial/FreeRTOSConfig.h com o seguinte conteúdo:

/*
 * FreeRTOS V202104.00
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * https://www.FreeRTOS.org
 * https://github.com/FreeRTOS
 *
 */
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
* Application specific definitions.
*
* These definitions should be adjusted for your particular hardware and
* application requirements.
*
* THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
* FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.  See
* http://www.freertos.org/a00110.html
*----------------------------------------------------------*/

#define configUSE_PREEMPTION                       1
#define configUSE_PORT_OPTIMISED_TASK_SELECTION    0
#define configUSE_IDLE_HOOK                        0
#define configUSE_TICK_HOOK                        0
#define configUSE_DAEMON_TASK_STARTUP_HOOK         0
#define configTICK_RATE_HZ                         ( 1000 )                  /* In this non-real time simulated environment the tick frequency has to be at least a multiple of the Win32 tick frequency, and therefore very slow. */
#define configMINIMAL_STACK_SIZE                   ( ( unsigned short ) 70 ) /* In this simulated case, the stack only has to hold one small structure as the real stack is part of the win32 thread. */
#define configTOTAL_HEAP_SIZE                      ( ( size_t ) ( 65 * 1024 ) )
#define configMAX_TASK_NAME_LEN                    ( 12 )
#define configUSE_TRACE_FACILITY                   1
#define configUSE_16_BIT_TICKS                     0
#define configIDLE_SHOULD_YIELD                    1
#define configUSE_MUTEXES                          1
#define configCHECK_FOR_STACK_OVERFLOW             0
#define configUSE_RECURSIVE_MUTEXES                1
#define configQUEUE_REGISTRY_SIZE                  20
#define configUSE_APPLICATION_TASK_TAG             1
#define configUSE_COUNTING_SEMAPHORES              1
#define configUSE_ALTERNATIVE_API                  0
#define configUSE_QUEUE_SETS                       1
#define configUSE_TASK_NOTIFICATIONS               1
#define configSUPPORT_STATIC_ALLOCATION            0

/* Software timer related configuration options.  The maximum possible task
 * priority is configMAX_PRIORITIES - 1.  The priority of the timer task is
 * deliberately set higher to ensure it is correctly capped back to
 * configMAX_PRIORITIES - 1. */
#define configUSE_TIMERS                           1
#define configTIMER_TASK_PRIORITY                  ( configMAX_PRIORITIES - 1 )
#define configTIMER_QUEUE_LENGTH                   20
#define configTIMER_TASK_STACK_DEPTH               ( configMINIMAL_STACK_SIZE * 2 )

#define configMAX_PRIORITIES                       ( 7 )

/* Run time stats gathering configuration options. */
unsigned long ulGetRunTimeCounterValue( void ); /* Prototype of function that returns run time counter. */
void vConfigureTimerForRunTimeStats( void );    /* Prototype of function that initialises the run time counter. */
#define configGENERATE_RUN_TIME_STATS             0

/* Co-routine related configuration options. */
#define configUSE_CO_ROUTINES                     0
#define configMAX_CO_ROUTINE_PRIORITIES           ( 2 )

/* This demo can use of one or more example stats formatting functions.  These
 * format the raw data provided by the uxTaskGetSystemState() function in to human
 * readable ASCII form.  See the notes in the implementation of vTaskList() within
 * FreeRTOS/Source/tasks.c for limitations. */
#define configUSE_STATS_FORMATTING_FUNCTIONS      0

/* Enables the test whereby a stack larger than the total heap size is
 * requested. */
#define configSTACK_DEPTH_TYPE                    uint32_t

/* Set the following definitions to 1 to include the API function, or zero
 * to exclude the API function.  In most cases the linker will remove unused
 * functions anyway. */
#define INCLUDE_vTaskPrioritySet                  1
#define INCLUDE_uxTaskPriorityGet                 1
#define INCLUDE_vTaskDelete                       1
#define INCLUDE_vTaskCleanUpResources             0
#define INCLUDE_vTaskSuspend                      1
#define INCLUDE_vTaskDelayUntil                   1
#define INCLUDE_vTaskDelay                        1
#define INCLUDE_uxTaskGetStackHighWaterMark       1
#define INCLUDE_uxTaskGetStackHighWaterMark2      1
#define INCLUDE_xTaskGetSchedulerState            1
#define INCLUDE_xTimerGetTimerDaemonTaskHandle    1
#define INCLUDE_xTaskGetIdleTaskHandle            1
#define INCLUDE_xTaskGetHandle                    1
#define INCLUDE_eTaskGetState                     1
#define INCLUDE_xSemaphoreGetMutexHolder          1
#define INCLUDE_xTimerPendFunctionCall            1
#define INCLUDE_xTaskAbortDelay                   1

#endif /* FREERTOS_CONFIG_H */

Detalhes sobre as opções de configuração podem ser encontradas na documentação oficial em

https://www.freertos.org/Documentation/RTOS_book.html

7.5 Criando um projeto novo: finalizando a aplicação "Hello World"

O nosso projeto está quase pronto, falta apenas o código principal do nosso programa. Para inseri-lo, crie o arquivo freertos-tutorial/main.c com o conteúdo

/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"

/*** SEE THE COMMENTS AT THE TOP OF THIS FILE ***/
int main( void )
{
    /* Perform any hardware setup necessary. (Not necessary for Desktop) */
    /* prvSetupHardware(); */
 
    /* --- APPLICATION TASKS CAN BE CREATED HERE --- */
 
    /* Start the created tasks running. */
    vTaskStartScheduler();
 
    /* Execution will only reach here if there was insufficient heap to
    start the scheduler. */
    for( ;; );
    return 0;
}
/*-----------------------------------------------------------*/

Estamos pronto para compilar! Primeiro, vamos fazer esse processo manualmente, compilando os arquivos .c um de cada vez para gerar os arquivos objetos .o. Para tal, execute os comandos:

$ cd freertos-tutorial
$ gcc -Wall -I. -I./include -c main.c -o main.o
$ gcc -Wall -I. -I./include -c tasks.c -o tasks.o
$ gcc -Wall -I. -I./include -c queue.c -o queue.o
$ gcc -Wall -I. -I./include -c list.c -o list.o
$ gcc -Wall -I. -I./include -c timers.c -o timers.o
$ gcc -Wall -I. -I./include -c event_groups.c -o event_groups.o
$ gcc -Wall -I. -I./include -c heap_4.c -o heap_4.o
$ gcc -Wall -I. -I./include -c port.c -o port.o

Agora vamos executar o linker para gerar o executável. A entrada do linker deve ser feita em todos os arquivos objetos, então o comando é

$ gcc main.o tasks.o queue.o list.o timers.o event_groups.o heap_4.o port.o -lWinmm -o freertos-tutorial.exe

Se não ocorreu nenhum erro de compilação ou linker, a sua instalação deve estar correta! Você pode tentar executar a aplicação com:

$ .\freertos-tutorial.exe

Mas nada deve ocorrer, pois nossa aplicação fica em laço infinito. Caso queira observar um programa de verdade em execução, copie um dos códigos do próximo capítulo, compile e execute.

7.6 Criando um projeto novo: automatizando a construção com Makefile (opcional)

O processo de construção do executável feito na seção anterior é bastante trabalhoso. Imagine que toda vez que modificar algum arquivo é preciso repetir o processo, pelo menos parcialmente. Para facilitar o desenvolvimento, vamos utilizar o sistema de build Makefiles, como feito na Apostila de C, na seção 9.1.

Para tal, primeiro apague todos os arquivos de saída da compilação com o comando:

No powershell:
$ rm *.o, *.

Ou, no prompt de comando:
$ del *.o *.exe

Depois, crie o arquivo freertos-tutorial/Makefile com o conteúdo

# Compilador
CC := gcc

# Nome do executavel
TARGET = freertos-tutorial.exe

# Arquivos fonte .c
SOURCES = main.c \
	tasks.c \
	queue.c \
	list.c \
	timers.c \
	event_groups.c \
	heap_4.c \
	port.c \

# Diretorios de busca para #include
INCLUDE_DIRS := -I.
INCLUDE_DIRS += -I./include

# Flags para a compilacao (geracao dos arquivos .o)
CFLAGS := -Wall

# Flags para o linker (.o's para executavel)
LDFLAGS := -lWinmm

OBJS = $(SOURCES:.c=.o)

.PHONY: build
build: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(OBJS) $(LDFLAGS) -o $@

%.o : %.c
	$(CC) $(CFLAGS) $(INCLUDE_DIRS) -c $< -o $@

.PHONY: clean
clean:
	del $(OBJS) $(TARGET)
#	For non Windows users:
#	rm -f $(OBJS) $(TARGET)

Com isso, para construir o seu projeto com o make, execute:

$ mingw32-make build

Que deve imprimir as seguintes mensagens:

gcc -Wall -I. -I./include -c main.c -o main.o
gcc -Wall -I. -I./include -c tasks.c -o tasks.o
gcc -Wall -I. -I./include -c queue.c -o queue.o
gcc -Wall -I. -I./include -c list.c -o list.o
gcc -Wall -I. -I./include -c timers.c -o timers.o
gcc -Wall -I. -I./include -c event_groups.c -o event_groups.o
gcc -Wall -I. -I./include -c heap_4.c -o heap_4.o
gcc -Wall -I. -I./include -c port.c -o port.o
gcc main.o tasks.o queue.o list.o timers.o event_groups.o heap_4.o port.o -lWinmm -o freertos-tutorial.exe

e gerar o executável freertos-tutorial/freertos-tutorial.exe.

Note que os comandos foram os mesmos executados na seção anterior. Uma vantagem de utilizar um sistema de build é que, ao modificar um arquivo, o make detecta automaticamente qual(is) arquivo(s) precisa(m) ser recompilado(s).

Por fim, é possível remover todos os arquivos do build com o comando:

$ mingw32-make clean

Bibliografia para este capítulo

8. FreeRTOS: Gerenciamento de Tasks

Assim como o nosso escalonador, o FreeRTOS também trabalha com o conceito de ticks e tarefas. O tick é a unidade de tempo discreta, gerada a partir de uma interrupção, que, por sua vez, pode ocasionar o chaveamento de uma tarefa para outra.

Diferentemente do nosso Task Switcher, o FreeRTOS admite preempção de tarefas. Ou seja, permite suspender a execução de uma tarefa e resumi-la mais adiante. Com isso, nossas tarefas podem ser implementadas como programas independentes, em um laço infinito.

8.1 Criando tarefas

Vamos demonstrar o escalonamento com preempção a partir de um programa bem simples, que cria duas tarefas. No código do main.c, vamos criar duas tarefas,

#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"

#define DELAY_LOOP_COUNT 100000000

void vTask1( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 1 is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

void vTask2( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 2 is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

que basicamente imprimem uma mensagem na tela e depois entram em um loop de espera ativa.

Para criar as tarefas, é possível usar a função xTaskCreate. Para tal, dentro do main(), insira o seguinte código:

int main( void )
{
    /* --- APPLICATION TASKS CAN BE CREATED HERE --- */
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL);
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL);
 
    /* Start the created tasks running. */
    vTaskStartScheduler();
 
    /* Execution will only reach here if there was insufficient heap to
    start the scheduler. */
    for( ;; );
    return 0;
}

onde os argumentos, em ordem, são: ponteiro para a função (vTask1), nome da função ("Task 1"), tamanho do stack em bytes (1000), argumentos para a tarefa (NULL), a prioridade (1) e um handler para a tarefa (NULL). Mais tarde estudaremos a influência da prioridade; os demais parâmetros podem ser consultados na documentação do FreeRTOS.

Ao compilar e executar este código, temos a seguinte saída:

Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
...

o que indica que o escalonador está dividindo o tempo entre as tarefas, de modo a dar a aparência de execução simultânea.

Realmente, quando executamos duas tarefas de mesma prioridade, o FreeRTOS faz o chamada time slicing, chaveando a tarefa em execução a cada tick, como mostrado na Fig.:

onde cada divisão de tempo corresponde ao intervalo dos ticks. Na prática, o que ocorre é que, a cada interrupção de tick, o kernel é chamado para determinar a tarefa a ser executada, como mostrado na Fig.:

8.2 Prioridade de tarefas

Para determinar a tarefa a ser executada, o kernel sempre escolhe a de maior prioridade. Caso haja empate, ele faz o time slicing, como mostrado no exemplo anterior.

Vamos testar essa funcionalidade. Modifique a seguinte linha da aplicação anterior:

xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL);

para:

xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, NULL);

Isto é, altere a prioridade da tarefa 2 para 2, que agora é maior do que a da tarefa 1.

Ao compilar e executar essa aplicação, temos a seguinte saída:

Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
...

que corresponde ao seguinte gráfico de execução:

Ou seja, apenas a tarefa 2 está sendo executada. Isto é esperado, pois a sua prioridade é maior e, portanto, é sempre escolhida pelo escalonador.

8.3 Usando o estado bloqueado para criar atrasos

Você deve estar se perguntando agora: como fazer para que a tarefa 1 execute, uma vez que esta possui prioridade inferior? Para responder esta pergunta, precisamos introduzir o conceito de estado de uma tarefa, mostrados na seguinte máquina de estados:

Observando os estados, verificamos que tarefas bloqueadas não podem ser selecionadas pelo kernel para execução. Assim, podemos atualizar o comportamento do escalonador: a tarefa a ser escolhida é a de maior prioridade entre as tarefas não bloqueadas. Caso possua mais de uma, é feito o time slicing (como visto na seção 8.1).

Uma forma de bloquear uma tarefa é criar um atraso (delay) com a função vTaskDelay, que recebe a duração em número de ticks como argumento. Para adaptar a nossa aplicação, vamos modificar as tarefas:

void vTask1( void *pvParameters )
{
  const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
  for( ;; )
  {
    printf("Task 1 is running\n");
    vTaskDelay(xDelay1000ms);
  }
}

void vTask2( void *pvParameters )
{
  const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
  for( ;; )
  {
    printf("Task 2 is running\n");
    vTaskDelay(xDelay1000ms);
  }
}

onde a função pdMS_TO_TICKS é utilizada para fazer a conversão de milissegundos para número de ticks. Ou seja, vTaskDelay(xDelay1000ms) cria um atraso de 1s.

A saída do programa é:

Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
...

que é semelhante a da versão com time slicing. No entanto, o gráfico de execução é bem diferente, como pode ser visto na Fig.:

Observe que, uma vez que todas as tarefas estão bloqueadas, a tarefa idle, que é uma tarefa criada automaticamente e basicamente não faz nada, é executada.

Comparando a execução da aplicação da seção 8.1 com o desta seção, nota-se que a última é muita mais eficiente, pois ocupa apenas uma fração do processador com as tarefas criadas. Em contraste, a aplicação anterior utilizava 100% do processador para executar as duas tarefas definidas.

8.4 Criando tarefas periódicas

A função de atraso da seção anterior permite criar um intervalo definido entre o final de uma iteração da tarefa e o início da próxima iteração. Para criar uma tarefa realmente periódica, em que o início de cada iteração ocorre em intervalos regulares, como o do capítulo anterior, podemos utilizar a função vTaskDelayUntil().

Para ilustrar melhor esta funcionalidade, vamos usar as duas tarefas contínuas executando em time slicing da seção 8.1.

#define DELAY_LOOP_COUNT 100000000
void vTask1( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 1 (continuous) is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

void vTask2( void *pvParameters )
{
  volatile uint32_t ul;
  for( ;; )
  {
    printf("Task 2 (continuous) is running\n");
    for( ul = 0; ul < DELAY_LOOP_COUNT; ul++ ) {
    }
  }
}

Além dessas, vamos criar uma tarefa periódica:

void vTask3( void *pvParameters )
{
  const TickType_t xDelay2000ms = pdMS_TO_TICKS( 2000 );
  TickType_t xLastWakeTime;
  xLastWakeTime = xTaskGetTickCount();
 
  for( ;; )
  {
    printf("Task 3 (periodic) is running\n");
    vTaskDelayUntil(&xLastWakeTime, xDelay2000ms);
  }
}

Para a tarefa periódica, devemos criar a variável xLastWakeTime para armazenar o instante de tempo da última execução. A função vTaskDelayUntil() recebe esta informação e a duração do atraso, que será de 2s. Além de realizar o delay, esta função atualiza automaticamente a variável xLastWakeTime.

A execução da aplicação deve render a seguinte saída:

Task 3 (periodic) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
Task 3 (periodic) is running
Task 2 (continuous) is running
Task 1 (continuous) is running
...

Cujo diagrama de execução é exibido a seguir:

Bibliografia para este capítulo

9. FreeRTOS: Comunicação entre Tarefas

Nas atividades anteriores, as tarefas executavam de forma independente. No entanto, muitas vezes é desejável alguma comunicação entre elas. Nesta apostila, vamos considerar apenas duas situações: o compartilhamento de recursos e a sincronização entre tarefas.

9.1 Compartilhamento de recursos: exclusão mútua

Quando duas tarefas atualizam uma mesma variável (recurso), precisamos empregar uma técnica de exclusão mútua. O objetivo é assegurar que, uma vez que uma tarefa acesse um recurso compartilhado, esta tenha acesso exclusivo até concluir sua operação. Desse modo, é garantida a consistência dos dados.

Para entender a necessidade deste processo, considere o seguinte exemplo:

int global_var = 0;

void vTask( void *pvParameters )
{
  for( ;; )
  {
    global_var++;
  }
}

int main( void )
{
    /* --- APPLICATION TASKS CAN BE CREATED HERE --- */
    xTaskCreate( vTask, "Task 1", 1000, NULL, 1, NULL);
    xTaskCreate( vTask, "Task 2", 1000, NULL, 1, NULL);
 
    /* Start the created tasks running. */
    vTaskStartScheduler();
 
    /* Execution will only reach here if there was insufficient heap to
    start the scheduler. */
    for( ;; );
    return 0;
}

Com uma análise superficial, não é possível detectar nenhuma inconsistência no programa. No entanto, a seguinte instrução em C:

global_var++;

equivale a múltiplas instruções na CPU.

Vamos considerar que a operação incremento realiza os seguintes três passos:

  1. lê o valor da memória e armazena em um registrador (READ);
  2. incrementa o valor no registrador (INC); e
  3. escreve o valor do registrador na memória (WRITE).

Agora, imagine a seguinte sequência de operações das tarefas, que corresponde a duas iterações da tarefa 1 e uma da tarefa 2:

Tarefa operação registrador global_var
Task 1 READ 0 0
Task 1 INC 0 → 1 0
Task 1 WRITE 1 0 → 1
Task 2 READ 1 1
Task 1 READ 1 1
Task 1 INC 1 → 2 1
Task 1 WRITE 2 1 → 2
Task 2 INC 1 → 2 1
Task 2 WRITE 2 1 → 2

Como podemos observar, o valor final de global_var foi 2, quando deveria ser 3 após três incrementos. A solução passa por realizar o incremento como uma operação atômica, ou seja, que não pode ser interrompida.

Para solucionar este problema, podemos utilizar um mutex, que funciona como uma fechadura controlando o acesso ao recurso. Quando uma tarefa deseja acessar o recurso, ela "trava" o mutex, impossibilitando outras tarefas de acessá-lo.

No FreeRTOS, mutex podem ser criados com a função xSemaphoreCreateMutex, "travados" com a função xSemaphoreTake() e destravados com xSemaphoreGive(). Quando alguma outra tarefa deseja acessar um recurso travado por um mutex, ela entra no estado bloqueado até a liberação do mutex ou um timeout. Este procedimento é ilustrado na Fig.:

Assim, o programa anterior pode ser reescrito com:

#include <stdio.h>
#include <stdlib.h>

/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

int global_var = 0;
SemaphoreHandle_t xMutex;

void vTask( void *pvParameters )
{
  const TickType_t xMaxBlockTimeTicks = 0x20;
  for( ;; )
  {
    xSemaphoreTake( xMutex, portMAX_DELAY ); // will block if mutex is already taken
    global_var++;
    printf("%d\n", global_var);
    xSemaphoreGive( xMutex );
    vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
  }
}

int main( void )
{
  xMutex = xSemaphoreCreateMutex();

  /* Check the semaphore was created successfully before creating the tasks. */
  if( xMutex != NULL ) {
    /* --- APPLICATION TASKS CAN BE CREATED HERE --- */
    xTaskCreate( vTask, "Task 1", 1000, NULL, 1, NULL);
    xTaskCreate( vTask, "Task 2", 1000, NULL, 1, NULL);

    /* Start the created tasks running. */
    vTaskStartScheduler();
  }
  
  /* Execution will only reach here if there was insufficient heap to
  start the scheduler. */
  for( ;; );
  return 0;
}

onde foi adicionado um delay aleatório para simular acessos não regulares ao recurso.

9.2 Sincronização entre tarefas com semáforo binário

O semáforo binário é bastante similar ao mutex, pois possui dois estados e faz o bloqueio da tarefa caso não consiga obtê-lo.

No FreeRTOS, o semáforo binário é utilizado para sincronizar tarefas. Neste caso, a tarefa a ser executada fica em estado bloqueado esperando o semáforo. Outra tarefa é encarregada de "fornecer" o semáforo, sinalizando para aquela que deve iniciar o processamento. É mais fácil compreender esta situação a partir da Fig.:

O programa abaixo contém um exemplo de uso de um semáforo binário para sincronização:

#include <stdio.h>
/* Kernel includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

#define BEEP_COUNT 5
SemaphoreHandle_t xBinarySemaphore;

void vTask1( void *pvParameters )
{
  volatile char buf[10];
  for( ;; )
  {
    buf[0] = '\0';
    scanf("%s", buf);
    if(buf[0]=='b')
      xSemaphoreGive(xBinarySemaphore);
  }
}

void vTask2( void *pvParameters )
{
  volatile uint32_t ul;
  const TickType_t xDelay1000ms = pdMS_TO_TICKS(1000);
  
  for( ;; )
  {
    xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
    for(ul = 0; ul < BEEP_COUNT; ul++) {
      printf("Beep!\n");
      vTaskDelay(xDelay1000ms);
    }
  }
}

int main( void )
{
  xBinarySemaphore = xSemaphoreCreateBinary();
  
  if(xBinarySemaphore != NULL) {
    /* --- APPLICATION TASKS CAN BE CREATED HERE --- */
    xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL);
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, NULL);
   
    /* Start the created tasks running. */
    vTaskStartScheduler();
  }
 
  /* Execution will only reach here if there was insufficient heap to
  start the scheduler. */
  for( ;; );
  return 0;
}

A tarefa 1 espera por uma entrada de teclado, enquanto a tarefa 2 fica em estado bloqueado. Caso seja digitado b, o semáforo é sinalizado e, com isso, a tarefa 2 sai do bloqueio e entra em execução, pois possui prioridade mais alta. Após a execução, a tarefa 2 se bloqueia novamente e a tarefa contínua 1 entra em execução.

9.3 Criando uma fila de eventos para a aplicação Alarme

A fila do FreeRTOS permite combinar a sincronização com o compartilhamento de recursos. O seu funcionamento é similar ao de uma fila FIFO (first in, first out) tradicional.

Em geral, uma (ou mais) tarefa(s) é(são) responsável(is) por inserir itens no final da fila e outra(s) por remover itens do início dela. Assim como no caso do semáforo binário, a tarefa pode esperar em estado bloqueado caso não exista item na fila.

Vamos adaptar nossa aplicação alarme para utilizar uma fila de eventos para a máquina de estados. Para declarar a fila, primeiro precisamos incluir os headers apropriados e criar um handler do tipo QueueHandle_t com:

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

QueueHandle_t xQueue;

Serão definidas duas tarefas: a primeira gera os eventos e põe na fila, e a segunda é a máquina de estados que consome e processa esses eventos da fila. A primeira tarefa pode ser escrita como:

void taskObterEvento(void *pvParameters) {
  int codigoEvento;
  BaseType_t xStatus;

  for( ;; ) {
    codigoEvento = NENHUM_EVENTO;

    teclas = ihm.obterTeclas();
    if (decodificarAcionar()) {
      codigoEvento = ACIONAR;
      xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
      if( xStatus != pdPASS )
        Serial.println("Erro ao enviar evento para fila");
      continue;
    }
    if (decodificarDesacionar()) {
      codigoEvento = DESACIONAR;
      xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
      if( xStatus != pdPASS )
        Serial.println("Erro ao enviar evento para fila");
      continue;
    }
    if (decodificarTimeout()) {
      codigoEvento = TIMEOUT;
      xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
      if( xStatus != pdPASS )
        Serial.println("Erro ao enviar evento para fila");
      continue;
    }
    if (decodificarDisparar()) {
      codigoEvento = DISPARAR;
      xStatus = xQueueSendToBack( xQueue, &codigoEvento, 0 );
      if( xStatus != pdPASS )
        Serial.println("Erro ao enviar evento para fila");
      continue;
    }
  }
}

onde a função xQueueSendToBack é usada para enviar o evento para a fila.

Já a tarefa de máquina de estados é alterada para:

void taskMaqEstados(void *pvParameters) {
  int codigoEvento;

  for( ;; ) {
    if( xQueueReceive( xQueue, &codigoEvento, portMAX_DELAY ) == pdPASS ) {
      if (codigoEvento != NENHUM_EVENTO)
      {
        codigoAcao = obterAcao(estado, codigoEvento);
        estado = obterProximoEstado(estado, codigoEvento);
        executarAcao(codigoAcao);
        printf("Estado: %d Evento: %d Acao:%d\n", estado, codigoEvento, codigoAcao);
      }
    }
    else {
      printf("Erro ao receber evento da fila\n");
    }
  }
}

Por fim, devemos inicializar a fila e as tarefas no main():

int main() {
  iniciaSistema();
  printf ("Alarme iniciado\n");

  // configure tasks
  xQueue = xQueueCreate(5, sizeof(int));
  if(xQueue != NULL)
  {
    xTaskCreate(taskMaqEstados,"taskMaqEstados", 1500, NULL, 2, &xTaskMaqEstados);
    xTaskCreate(taskObterEvento,"taskObterEvento", 1000, NULL, 1, &xTaskObterEvento);
    vTaskStartScheduler();
  }
  else
  {
    /* The queue could not be created. */
  }
} // main

Note que, como a prioridade da tarefa taskMaqEstados é a maior, a fila nunca terá mais de um elemento. Isto porque, uma vez inserido algum evento nela, a tarefa da máquina de estados é imediatamente desbloqueada e executada, consumindo o evento. Depois de processar o evento, a tarefa prioritária entra em estado de bloqueio e a taskObterEvento volta a executar.

A aplicação final do Alarme no FreeRTOS pode ser consultada em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_freertos

Bibliografia para este capítulo