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 seu código precisa ser executado a partir deste loop, e as coisas vão ficando complicadas/bagunçadas a medida que você liga outros sensores, servos, LEDs e shields que necessitam uma 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 idéia 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 a sensação de multitarefa. Normalmente os processos são trocados no processador milhares de vezes em apenas um segundo.
Boa parte destes sistemas operacionais também priorizam os processos de forma a utilizar de forma eficiente os recursos de IO (onde há interação com usuário ou dispositivos externos) e assim resolver no menor tempo possível todas as tarefas. 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 criança de AOScheduler e disponibilizo no github (https://github.com/phcco/arduino-scheduler), basta descompactar dentro da pasta libraries
no diretório da IDE.
A idéia desta biblioteca é facilitar o desenvolvimento de projetos com mais de uma funcionalidade, para tal, cada funcionalidade/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
loop
das tarefas devem sempre retornarAO_OK
se a tarefa precisa ser reescalonada, se esta tarefa retornarAO_EXIT
ela 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.