Escalonador de tarefas no Arduino

18/03/2012 por

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.

A prática

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.

Multitask?

Muitas pessoas pensam que sistemas multitarefas são sistemas que executam programas em paralelo, ERRADO!. 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.

Arduino multitarefa?

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.

Princípios da biblioteca

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 retornar AO_OK se a tarefa precisa ser reescalonada, se esta tarefa retornar AO_EXIT ela sairá da fila de execução e não será mais executada.

Algumas observações e trabalhos futuros

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.