Para quem não conhece, o Arduino é uma plataforma open-source que é composta de um hardware muito simples (um microprocessador RISC e um pouco de memória), e um kit de software tão simples quanto o hardware onde você pode programar em C e C++. Mais informações podem ser encontradas no site do Arduino (arduino.cc) ou na Wikipedia.
Para exemplificar o uso do equipamento a IDE carrega vários exemplos e um dos exemplos mais básicos é um aplicativo que faz um LED na própria placa piscar.
/*
Blink
Turns on an LED on for one second, then off for one second, repeatedly.
This example code is in the public domain.
*/
void setup() {
// initialize the digital pin as an output.
// Pin 13 has an LED connected on most Arduino boards:
pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH); // set the LED on
delay(1000); // wait for a second
digitalWrite(13, LOW); // set the LED off
delay(1000); // wait for a second
}Na prática o Arduino irá executar a função setup apenas uma vez, e depois irá executar a função loop para o resto da vida, enquanto o hardware estiver energizado.
Note que todo o seu código precisa ser executado a partir deste loop, e as coisas vão ficando complicadas ou bagunçadas à medida que você liga outros sensores, servos, LEDs e shields que necessitam de certa independência de execução. Um exemplo é quando desenvolve um processo para ler o ambiente com um sensor e outro processo para controle de movimento de um robô a partir do mapeamento do terreno, e resolve registrar periodicamente os sensores de temperatura do ambiente para criar um histórico.
Muitos desenvolvedores consideram que sistemas multitarefas necessariamente são sistemas que executam programas em paralelo, não é bem assim!. A maioria dos sistemas operacionais antes da popularização de processadores de núcleo duplo (lá por 2005) apenas compartilhavam o tempo de execução em um único processador, e o sistema operacional era (e ainda é) obrigado a escolher qual tarefa (processo) deve ser executado e quanto tempo ele pode ficar utilizando o processador.
A ideia básica é a seguinte: quanto menor a fatia de tempo que um processo utiliza o processador, mais processos terão chance de executar e, teoricamente, melhor será a sensação de multitarefa. Normalmente os processos são trocados no processador milhares de vezes em apenas um segundo.
Boa parte desses sistemas operacionais também prioriza os processos para utilizar de forma eficiente os recursos de I/O (quando há interação com o usuário ou dispositivos externos) e, assim, resolver todas as tarefas no menor tempo possível. Mas isso é outra história que não vem ao caso agora... passaria uma madrugada inteira realizando observações sobre isso.
Como o Arduino possui um hardware muito simples, você não pode utilizar um sistema operacional complexo para escalonar suas atividades nele, muito menos parar o processamento de uma tarefa pela metade para iniciar outra com maior prioridade, no máximo utilizar um port do FreeRTOS que é um pouco complicada de carregar e utilizar (pelo que li por aí, até parece suportar preempção, mas na prática não consegui chegar muito longe).
Para resolver meu problema, desenvolvi uma pequena biblioteca que faz o escalonamento de tarefas. Batizei-a de AOScheduler e disponibilizo-a no GitHub (https://github.com/phcco/arduino-scheduler). Basta descompactá-la dentro da pasta libraries, no diretório da IDE.
A ideia desta biblioteca é facilitar o desenvolvimento de projetos com mais de uma funcionalidade. Para isso, cada funcionalidade ou tarefa é chamada de Task e implementada na classe AOTask, que é escalonada pelo AOScheduler.
Para exemplificar a sua utilização, montei um exemplo onde é possível determinar que um LED fique aceso durante X tempo e desligado Y tempo. O código do MultiBlink pode ser visto abaixo (uma versão com comentários também está dentro da biblioteca).
#include "AOScheduler.h"
class MultiBlink : public AOTask {
private:
int _pin;
unsigned long _uptime,_downtime;
int _state;
public:
MultiBlink(int pin,unsigned long uptime, unsigned long downtime){
_pin = pin;
_uptime = uptime;
_downtime = downtime;
_state = LOW;
}
int setup(){
this->task_expected_period = _uptime;
pinMode(_pin, OUTPUT);
digitalWrite(_pin,_state = HIGH);
}
int loop(){
if(_state==LOW){
this->task_expected_period = _uptime;
_state = HIGH;
}else{
this->task_expected_period = _downtime;
_state = LOW;
}
digitalWrite(_pin, _state);
return AO_OK;
}
};
AOScheduler aos;
// Tempos em microsegundos
// MultiBlink blinker1(pin, tempo ligado, tempo desligado);
MultiBlink blinker1(13,SECOND,2*SECOND);
MultiBlink blinker2(12,MILLISECOND,1*SECOND);
MultiBlink blinker3(11,MILLISECOND,1*SECOND);
MultiBlink blinker4(9,SECOND/2,3*SECOND);
MultiBlink blinker5(8,MILLISECOND,1*SECOND);
MultiBlink blinker6(5,SECOND,4*SECOND);
void setup(){
aos.add(&blinker1);
aos.add(&blinker2);
aos.add(&blinker3);
aos.add(&blinker4);
aos.add(&blinker5);
aos.add(&blinker6);
}
void loop(){
aos.run();
}Na linha 1 incluímos a biblioteca AOScheduler, entre a linha 3 e 33 declaramos o comportamento da tarefa. Nesta tarefa a função setup é chamada apenas uma vez (ao ser adicionada ao escalonador) e a função loop é executada cada vez que a tarefa é escalonada. Note que isso faz com que a tarefa tenha uma execução atômica (não é interrompida no meio da execução do loop).
Por ser atômica, qualquer delay ou delayMicroseconds dentro da função pode atrasar a execução da próxima tarefa. O escalonador executa a função loop de acordo com o valor em microsegundos armazenado no atributo task_expected_period. Se você configurar para executar a cada 1000 microsegundos, o escalonador se esforçará para executar neste período, inclusive ajustando o tempo entre cada tarefa para aproximar o tempo de execução ao desejado.
Na linha 35 criamos o escalonador, e nas linhas 38 até 43 criamos várias tarefas do MultiBlink, onde o primeiro parâmetro é o pino digital que será utilizado para enviar o sinal, o segundo é o tempo que o LED deve permanecer ligado e o terceiro é o tempo que o LED deve permanecer desligado.
Na função setup adicionamos ao escalonador as tarefas e no loop iniciamos a execução do escalonador. O método run só retornará quando não houver mais tarefas a serem executadas.
O método
loopdas tarefas devem sempre retornarAO_OKse a tarefa precisa ser reescalonada, se esta tarefa retornarAO_EXITela sairá da fila de execução e não será mais executada.
Como este escalonador trabalha com timers para executar suas tarefas, não espere que as execuções respeitem fielmente os prazos, é um escalonador de tempo compartilhado e portanto pode falhar.
A idéia agora é melhorar a biblioteca tentando acelerar o processo de escalonamento e ajustar os temporizadores para evitar a desincronia das tarefas, pois o tempo de escalonamento ainda não é contabilizado nestes timers.
A implementação de tarefas que emulam interrupções também estão na lista, inclusive em uma versão beta anexada a biblioteca, apresentarei em outro artigo.