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:
- Compilar com optimização por tamanho (-Os). O Arduino IDE já tem esta flag ativa por omissão.
- Usar variáveis locais sempre que possível.
- Usar o tipo de dados mais pequeno aplicável. Usar variáveis sem sinal se possível.
- Se uma variável não local apenas é referenciada dentro de função deverá ser declarada estática.
- Juntar variáveis não locais em estruturas sempre que seja natural. Isto aumenta a possibilidade de endereçamento indirecto sem recarregamento do apontador.
- Usar apontadores com deslocamento (offset) ou declarar estruturas para aceder ao I/O memory mapped.
- Usar o Use for(;;) { } para ciclos externos.
- Usar o do { } while(expressão) se aplicável.
- Usar contadores descendentes e pre-decrementos se aplicável.
- Aceder à memory de I/O directamente (i.e., não usar apontadores).
- Declarar a main como C_task se não for chamada de nenhum lado do programa.
- Usar macros em vez de funções para tarefas que criem menos de 2-3 linhas de código assembler.
- 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.
- 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.
- 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.
- 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).
- Se possível, evite chamar funções de dentro da rotina de interrupção.
- 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:
Tipo | Intervalo de valores | Nº de Bytes |
---|---|---|
char | -128 a 127 | 1 |
byte | 0 a 255 | 1 |
int | -32,768 a 32,767 | 2* |
unsigned int | 0 a 65,535 | 2* |
word | 0 a 65,535 | 2 |
long | -2,147,483,648 a 2,147,483,647 | 4 |
unsigned long | 0 a 4,294,967,295 | 4 |
float | -3.4028235E+38 a 3.4028235E+38 | 4 |
double | -3.4028235E+38 a 3.4028235E+38 | 4* |
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.