Programar

Arquitetura Modular

Um sistema como o do aeropêndulo envolve vários componentes (sensor, atuador, driver, controlador, interface).
Se o código for escrito de forma monolítica, rapidamente se torna difícil de entender, testar e manter.

Uma arquitetura modular baseada em máquinas de estado permite:

  • Que terceiros compreendam facilmente a lógica do programa e das suas partes.

  • Testar cada módulo de forma independente.

  • Garantir que diferentes tarefas (ex.: leitura de sensores, comando do motor, interface com utilizador) não se bloqueiam mutuamente.

  • Evitar o uso de delay(), que pode ser fatal num sistema de controlo em tempo real.

  • Usar de forma eficiente os recursos de hardware do Arduino, como o gerador PWM ou as interrupções, libertando o processador de tarefas críticas temporizadas.


Introdução a Máquinas de Estado

Uma máquina de estados finitos (FSM – Finite State Machine) é um modelo que organiza o comportamento de um sistema em estados, com transições entre eles, desencadeadas por eventos.

Elementos principais:

  • Estados: configurações em que o sistema pode estar (ex.: “verde”, “amarelo”, “vermelho”).

  • Estado inicial: ponto de partida.

  • Transições: regras de mudança de estado.

  • Eventos: condições que causam a transição.

  • Estado final: opcional, marca o fim da execução.

Exemplo genérico: um semáforo alterna entre verde, amarelo e vermelho, com tempos definidos.


Exemplo 1 — Dois LEDs a Piscar em Concorrência

Um primeiro exercício útil é piscar dois LEDs com frequências diferentes. Cada LED é controlado por uma máquina de estado independente, mas ambas executam em concorrência dentro do loop().

#define LED1 2
#define LED2 3

float f1 = 1.0;   // Frequência LED1 [Hz]
float f2 = 2.0;   // Frequência LED2 [Hz]
int duty = 50;    // Duty cycle (%)

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  Serial.begin(115200);
}

void loop() {
  autoBlink(LED1, f1, duty);
  autoBlink(LED2, f2, duty);
}

void autoBlink(int pin, float f, int duty) {
  const int APAGADO = 0, ACESO = 1;
  static int state = APAGADO;
  static unsigned long t0 = millis();

  int T = 1000 / f;
  int TH = T * duty / 100;
  int TL = T - TH;

  switch (state) {
    case APAGADO:
      if (millis() - t0 > TL) {
        digitalWrite(pin, HIGH);
        t0 = millis();
        state = ACESO;
      }
    break;
    case ACESO:
      if (millis() - t0 > TH) {
        digitalWrite(pin, LOW);
        t0 = millis();
        state = APAGADO;
      }
    break;
  }
}

Exemplo 2 — Máquina de Estado Temporal (Protocolo)

Além de FSMs que descrevem sequências de estados físicos, também podemos ter máquinas de estado que implementam protocolos temporais.

Exemplo: criar uma rampa de valores PWM para gerar uma forma de onda triangular (útil em ensaios de calibração de motores).

#define MOTOR_PIN 5
int pwmValue = 0;
int step = 5;

void loop() {
  static unsigned long t0 = millis();
  unsigned long T = 50; // passo de 50 ms

  if (millis() - t0 > T) {
    pwmValue += step;
    if (pwmValue >= 255 || pwmValue <= 0) step = -step;
    analogWrite(MOTOR_PIN, pwmValue);
    t0 = millis();
  }
}

Este tipo de FSM é útil para ensaios automáticos (ex.: rampa ou senoide) sem intervenção do utilizador.


Exemplo 3 — FSM baseada em Interação do Utilizador

Outra FSM típica responde a eventos externos, como o pressionar de um botão.

#define BUTTON_PIN 7
#define LED_PIN 8

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  static int state = 0;
  static int lastButton = HIGH;
  int button = digitalRead(BUTTON_PIN);

  if (lastButton == HIGH && button == LOW) { // deteta transição
    state = !state;
    digitalWrite(LED_PIN, state);
  }
  lastButton = button;
}

Esta FSM mostra a diferença entre um protocolo temporal (exemplo anterior) e um protocolo baseado em eventos externos.


Esqueleto para o Aeropêndulo

Segue uma estrutura modular que poderá ser usada como ponto de partida:

void setup() {
  initSensor();
  initMotor();
  initControl();
}

void loop() {
  readSensor();
  computeControl();
  commandMotor();
  logData();
}

Cada função pode ser implementada como uma máquina de estado própria, permitindo testar e substituir módulos sem afetar os restantes.


Interrupções no Arduino

O Arduino permite associar funções a interrupções de hardware, que são executadas automaticamente quando ocorre um evento (ex.: transição num pino digital, overflow de um timer).

Exemplo: Contagem de Pulsos (Encoder Simples)

#define ENCODER_PIN 2
volatile long counter = 0;

void ISR_encoder() {
  counter++;
}

void setup() {
  attachInterrupt(digitalPinToInterrupt(ENCODER_PIN), ISR_encoder, RISING);
  Serial.begin(115200);
}

void loop() {
  Serial.println(counter);
  delay(500);
}

Para encoders em quadratura, usam-se duas interrupções (canais A e B), permitindo detetar também o sentido de rotação.