No mundo dos micro-controladores e ao contrário do mundo dos computadores, os recursos são escassos e todas as optimizações que possam ser feitas são muito úteis.
Neste artigo iremos abordar algumas das várias optimizações que podem ser feitas para que o código final carregado no micro-controlador seja mais pequeno e potencialmente mais rápido.
Irei focar-me na optimização que pode ser obtida nos chips da família Atmega/Attiny. No entanto algumas destas abordagens podem ser seguidas para qualquer tipo de micro-controlador.

Partirei de um exemplo simples e iremos observar que com algumas alterações a redução de código produzido pelo compilador é muito significativa.

Existem 18 recomendações que globalmente diminuem o tamanho do código:

  1. Compilar com optimização por tamanho (-Os). O Arduino IDE já tem esta flag ativa por omissão.
  2. Usar variáveis locais sempre que possível.
  3. Usar o tipo de dados mais pequeno aplicável. Usar variáveis sem sinal se possível.
  4. Se uma variável não local apenas é referenciada dentro de função deverá ser declarada estática.
  5. Juntar variáveis não locais em estruturas sempre que seja natural. Isto aumenta a possibilidade de endereçamento indirecto sem recarregamento do apontador.
  6. Usar apontadores com deslocamento (offset) ou declarar estruturas para aceder ao I/O memory mapped.
  7. Usar o Use for(;;) { } para ciclos externos.
  8. Usar o do { } while(expressão) se aplicável.
  9. Usar contadores descendentes e pre-decrementos se aplicável.
  10. Aceder à memory de I/O directamente (i.e., não usar apontadores).
  11. Declarar a main como C_task se não for chamada de nenhum lado do programa.
  12. Usar macros em vez de funções para tarefas que criem menos de 2-3 linhas de código assembler.
  13. Reduzir o tamanho do Interrupt Vector segment (INTVEC) para o que é atualmente usado pela aplicação. Alternativamente, concatenar todos os CODE segments numa declaração e será feito automaticamente.
  14. A reutilização de código é intra-modular. Conjugue varias funções num módulo (i.e., num ficheiro) para aumentar o factor de reutilização de código.
  15. Em alguns casos, as optimizações por velocidade resultam em código de tamanho menor que a optimização por tamanho. Compilar módulo a módulo para verificar qual apresenta melhor resultado.
  16. Optimizar o C_startup para não inicializar segmentos não usados (i.e., IDATA0 ou IDATA1 se todas as variáveis são pequenas).
  17. Se possível, evite chamar funções de dentro da rotina de interrupção.
  18. Usar o modelo de memória menor possível.

Iremos fazer uso de algumas destas recomendações. Para se testar o resultado das optimizações iremos partir de um código base.

Todos os sketchs encontram-se num ZIP neste link

O seguinte código é uma versão modificada do Blink que vai piscar dois leds com um intervalo variável entre 0 e 1000 mseg com saltos de 100 em 100 mseg.

int led1 = 13;
int led2 = 12;
int delayTime = 1000;

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);  
}

void loop() {
  for (delayTime = 0; delayTime < 1000 ; delayTime += 100) {
    digitalWrite(led1, HIGH);
    delay(delayTime);
    digitalWrite(led1, LOW);
    delay(delayTime);
    digitalWrite(led2, HIGH);
    delay(delayTime);
    digitalWrite(led2, LOW);
    delay(delayTime);  
  }
}
// Sketch uses 1,198 bytes (3%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 15 bytes (0%) of dynamic memory, leaving 2,033 bytes for local variables. Maximum is 2,048 bytes.

Como podemos observar o código original ocupa 1196 bytes de flash.

Usando a optimização do espaço ocupado pelas variáveis iremos alterar o seu tipo:

TipoIntervalo de valoresNº de Bytes
char-128 a 1271
byte0 a 2551
int-32,768 a 32,7672*
unsigned int0 a 65,5352*
word0 a 65,5352
long-2,147,483,648 a 2,147,483,6474
unsigned long0 a 4,294,967,2954
float-3.4028235E+38 a 3.4028235E+384
double-3.4028235E+38 a 3.4028235E+384*

Os valores com (*) são para o chip Atmega328P. Noutros poderá ser diferente.

Passo 1

No exemplo podemos verificar que a declaração dos leds pode ser convertida em byte.

Adicionalmente pode igualmente ser convertida em const uma vez que não são alteradas (esta informação é essencial para o compilador optimizar a utilização destas variáveis).

Este passo é totalmente seguro e pode sempre ser feito.

const byte led1 = 13;
const byte led2 = 12;
int delayTime = 1000;

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);  
}

void loop() {
  for (delayTime = 0; delayTime < 1000; delayTime += 100) {
    digitalWrite(led1, HIGH);
    delay(delayTime);
    digitalWrite(led1, LOW);
    delay(delayTime);
    digitalWrite(led2, HIGH);
    delay(delayTime);
    digitalWrite(led2, LOW);
    delay(delayTime);  
  }
}
// Sketch uses 1,182 bytes (3%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 11 bytes (0%) of dynamic memory, leaving 2,037 bytes for local variables. Maximum is 2,048 bytes.

O ganho foi de 16 bytes.

Passo 2

Vamos agora passar a variável global delayTime para local da função loop.

Este passo é totalmente seguro e pode sempre ser feito.

const byte led1 = 13;
const byte led2 = 12;

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);  
}

void loop() {

  int delayTime;

  for (delayTime = 0; delayTime <= 1000; delayTime += 100) {
    digitalWrite(led1, HIGH);
    delay(delayTime);
    digitalWrite(led1, LOW);
    delay(delayTime);
    digitalWrite(led2, HIGH);
    delay(delayTime);
    digitalWrite(led2, LOW);
    delay(delayTime);  
  }
}
// Sketch uses 1,110 bytes (3%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 11 bytes (0%) of dynamic memory, leaving 2,037 bytes for local variables. Maximum is 2,048 bytes.

Desta vez o ganho foi de 72 bytes.

Passo 3

Neste passo iremos encapsular funções que fazem múltiplas operações.
Neste caso criou-se uma função para executar o trabalho de um dos leds.

Este passo é totalmente seguro e pode sempre ser feito.

const byte led1 = 13;
const byte led2 = 12;

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);  
}

void go(const byte led, const int delayTime) {
    digitalWrite(led, HIGH);
    delay(delayTime);
    digitalWrite(led, LOW);
    delay(delayTime);
}

void loop() {
  int delayTime;
  for (delayTime = 0; delayTime <= 1000; delayTime += 100) {
    go(led1, delayTime);
    go(led2, delayTime);
  }
}
// Sketch uses 1,102 bytes (3%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 9 bytes (0%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

Apenas ganhámos 8 bytes.

Passo 4

Temos que tomar medidas mais radicais. Isto passa por substituir as funções pinMode e digitalWrite.

Esta alteração quebra algumas funcionalidades nomeadamente a relacionada com o PWM.

Também não é feita verificação de valores incorrectos passados às macros.
Uma vez que vamos mexer directamente com os registos o código tem um conjunto de macros que permitem o uso do código com os seguintes microcontroladores:

  • Atmega328P
  • Atmega168
  • attiny45/85
  • attiny44/84

Este código é tendencialmente mais rápido porque faz o estritamente necessário.

Incluir o código no topo dos sketchs:

// AVR-optimize
//
#if defined (__AVR_ATtiny45__) || defined (__AVR_ATtiny85__) || defined (__AVR_ATtiny44__) || defined (__AVR_ATtiny84__)
#define portOfPin(P)\
  ((&PORTB))
#define ddrOfPin(P)\
  ((&DDRB))
#define pinOfPin(P)\
  ((&PINB))
#define pinIndex(P)((uint8_t)(P>13?P-14:P&7))
#else
#if (__AVR_ATtiny44__) || defined (__AVR_ATtiny84__)
#define portOfPin(P)\
  (((P)>=0&&(P)<8)?&PORTA:&PORTB)
#define ddrOfPin(P)\
  (((P)>=0&&(P)<8)?&DDRA:&DDRB)
#define pinOfPin(P)\
  (((P)>=0&&(P)<8)?&PINA:&PINB)
#define pinIndex(P)((uint8_t)(P>7?P-7:P&7))  
#else
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)

#define portOfPin(P)\
  (((P)>=0&&(P)<8)?&PORTD:(((P)>7&&(P)<14)?&PORTB:&PORTC))
#define ddrOfPin(P)\
  (((P)>=0&&(P)<8)?&DDRD:(((P)>7&&(P)<14)?&DDRB:&DDRC))
#define pinOfPin(P)\
  (((P)>=0&&(P)<8)?&PIND:(((P)>7&&(P)<14)?&PINB:&PINC))
#define pinIndex(P)((uint8_t)(P>13?P-14:P&7))
#endif
#endif
#endif

#define pinIndex(P)((uint8_t)(P>13?P-14:P&7))
#define pinMask(P)((uint8_t)(1<<pinIndex(P)))

#define pinAsInput(P) *(ddrOfPin(P))&=~pinMask(P)
#define pinAsInputPullUp(P) *(ddrOfPin(P))&=~pinMask(P);digitalHigh(P)
#define pinAsOutput(P) *(ddrOfPin(P))|=pinMask(P)
#define digitalLow(P) *(portOfPin(P))&=~pinMask(P)
#define digitalHigh(P) *(portOfPin(P))|=pinMask(P)
#define isHigh(P)((*(pinOfPin(P))& pinMask(P))>0)
#define isLow(P)((*(pinOfPin(P))& pinMask(P))==0)
#define digitalState(P)((uint8_t)isHigh(P))

Novo código:

// incluir o AVR-optimize

const byte led1 = 3;
const byte led2 = 2;

void setup() {
  pinAsOutput(led1);
  pinAsOutput(led2);  
}

void go(const byte led, const int delayTime) {
    digitalHigh(led);
    delay(delayTime);
    digitalLow(led);
    delay(delayTime);
}

void loop() {
  int delayTime;
  for (delayTime = 0; delayTime <= 1000; delayTime += 100) {
    go(led1, delayTime);
    go(led2, delayTime);
  }
}
// Sketch uses 812 bytes (2%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 9 bytes (0%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

Finalmente quebrámos a barreira do 1k.

Um sketch sem qualquer código ocupa 450 bytes. O nosso ocupa 812 bytes.

O que significa que o código que estamos a usar está a ocupar 362 bytes.

Passo 5

Ainda não substituímos uma das funções - delay. Neste passo fazemos essa substituição.

// incluir o AVR-optimize

const byte led1 = 13;
const byte led2 = 12;

void setup() {
  pinAsOutput(led1);
  pinAsOutput(led2);  
}

void tinyDelay(int time) {
  register unsigned long initial = millis();
  while ( millis() - initial < time ) {
    yield();
  }
}

void go(const byte led, const int delayTime) {
    digitalHigh(led);
    tinyDelay(delayTime);
    digitalLow(led);
    tinyDelay(delayTime);
}

void loop() {
  int delayTime;
  for (delayTime = 0;delayTime <= 1000;delayTime += 100) {
    go(led1, delayTime);
    go(led2, delayTime);
  }
}
// Sketch uses 744 bytes (2%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 9 bytes (0%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

Nesta última iteração conseguimos passar para 744 bytes.

Passo Final

Iremos neste passo fazer ainda mais algumas optimizações:

  • Criar o nosso próprio main
  • voltar a descrever as operações que a função go fazia eliminando a função completamente
  • eliminar o setup e o loop colocando o código diretamente no main
// incluir o AVR-optimize

const byte led1 = 13;
const byte led2 = 12;

void tinyDelay(int time) {
  register unsigned long initial = millis();
  while ( millis() - initial < time ) {
    yield();
  }
}

int main(void) {
  init(); // don't forget this!
  // SETUP:
  pinAsOutput(led1);
  pinAsOutput(led2);  
  for(;;) {
    // LOOP:
    int delayTime;
    for (delayTime = 0; delayTime <= 1000; delayTime += 100) {
      digitalHigh(led1);
      tinyDelay(delayTime);
      digitalLow(led1);
      tinyDelay(delayTime);
      digitalHigh(led2);
      tinyDelay(delayTime);
      digitalLow(led2);
      tinyDelay(delayTime);
    }
  } 
}
// Sketch uses 578 bytes (1%) of program storage space. Maximum is 32,256 bytes.
// Global variables use 9 bytes (0%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

Conclusão

Começamos com 1,198 bytes e conseguimos optimizar o código para 578 bytes - menos de metade do código original.

Este exercício deverá ser feito sempre que necessário ponderando todos os potenciais problemas que algumas das optimizações podem provocar.

Poderíamos ainda ter ido para o código plain sem bibliotecas do Arduino mas isso fica para outro artigo.