Otimização Memória Arduino

Arduino: Organização e Otimização da Memória

Veja nesse tutorial como está organizada a memória do Arduino e conheça  as principais técnicas de otimização.

Quem desenvolve programas para dispositivos com memória reduzida, precisa estar antenado com as técnicas de otimização de modo a evitar problemas de estabilidade e desempenho.
Neste artigo, reunimos importantes dicas que você pode utilizar em seus sketchs com o objetivo de economizar espaço em memória

Antes de explorarmos as técnicas aqui descritas veremos alguns conceitos básicos sobre a linguagem de programação do Arduino, tipos de dados e organização da memória.

A linguagem de Programação Arduino

Arduino não é somente uma plaquinha. É uma plataforma, um ecossistema completo formado por: Hardware, Software e uma filosofia.

A IDE do Arduino foi baseada na linguagem Processing, de onde surgiu o termo sketch e a linguagem adotada na IDE é baseada na linguagem Wiring, que é uma variação da linguagem C++/C, com diferenças significativas, as quais não são o escopo desse artigo. Além disso, o sistema Arduino foi baseado no compilador GCC, mais especificamente, o avr-gcc

Tipos de Dados

Os tipos de dados primitivos usados pela plataforma podem ser resumidos dessa forma:

    • bool/boolean ⇒ Tipo de dados lógico: 0 ou 1 (true/false)
    • int ⇒ Números inteiros
    • char ⇒ Armazena um e somente um caractere ASCII.
    • float/double ⇒ Números reais (com casas decimais)

Existem também alguns modificadores de tipos que permitem alterar a faixa de valores dos tipos de dados de acordo com necessidades específicas. São eles:

    • signed ⇒ Números com sinal (modificador default)
    • unsigned ⇒ Números sem sinal
    • short ⇒ Reduzir a faixa de valores
    • long ⇒ Aumenta a faixa de valores

Vejamos então a tabela com todos os tipos de dados primitivos do Arduino:

Tipo de Dado / Apelido Tamanho Faixa de valores
bool/boolean 1 byte 0 ou 1
char / signed char / int8_t 1 byte -128 até 127
byte / unsigned char / uint8_t 1 byte 0 até 255
int / short / int16_t 2 bytes -32,768 até 32,767
word / unsigned int / uint16_t 2 bytes 0 até 65,535
long / int32_t 4 bytes -2,147,483,648 até 2,147483,647
dword / unsigned long / uint32_t 4 bytes 0 até 4,294,967,295
long long 8 bytes -(2^63) até (2^63)-1
float / double 4 bytes 3.4028235E-38 até 3.4028235E+38
double 4 bytes 3.4028235E-38 até 3.4028235E+38

Observações:

  • Alguns tipos de dados como int são dependentes do processador e podem variar de tamanho. Por exemplo, o tipo int no UNO ocupa dois bytes, enquanto que no DUE ocupa 4 bytes.
  • Apelidos de tipos de dados como uint16_t estão definidos no arquivo stdint.h
  • Os tipos inteiros podem ser definidos com ou sem sinal. Dessa forma, unsigned long e uint32_t são equivalentes;
  • Os tipos double e float, na plataforma, possuem a mesma precisão e ocupam o mesmo espaço na memória;

Escopo de Variáveis

O escopo das variáveis determina sua visibilidade e tempo de vida dentro de um programa. A linguagem do Arduino define três tipos de escopo:

Global → As variáveis globais podem ser vistas (acessadas) em qualquer ponto do programa (incluindo todas a funções) e permanecem “vivas” durante todo o tempo de execução.
Para definir uma variável como global, simplesmente declare-a fora de qualquer função (antes do setup).

Local → Variáveis locais pode ser acessadas somente dentro da função em que foram definidas e seu tempo de vida termina quando a função termina.
Variáveis locais são declaradas dentro de funções.

Estático → Variáveis estáticas podem ser acessadas somente dentro da função em que foram definidas, mas seu tempo de vida é global, ou sejam, seu valor é preservado entre as chamadas à função.
Variáveis estáticas são declaradas com a storage class static.

Obs: A storage class external pode ser usada para permitir que uma variável seja compartilhada entre vários arquivos de código-fonte.

Organização da Memória

Os processadores da linha Atmega 328 (Arduino) adotam o modelo de Harvard (em oposição ao modelo Von Neumann) cuja arquitetura separa fisicamente a memória utilizada pelo programa da usada pelos dados (variáveis), de acordo com a seguinte disposição:

Memória Flash → Memória não volátil onde fica armazenado o programa (sketch). Você pode armazenar dados nesta memória, mas não pode alterá-los.

SRAM → Memória de leitura e escrita usada para armazenamentos dos dados dos programas (variáveis).

EEPROM → Memória não volátil onde podem ser armazenados e lidos dados byte a byte.

A memória SRAM

A memória SRAM é importante, pois é onde se passa toda a ação: Alocação estática e dinâmica de variáveis, ponteiros para chamadas de funções, etc.
É essa parte da memória que devemos concentrar nossos esforços de otimização, pois é aí que acontecem os problemas.

Vejamos como ela está dividida observando esse gráfico:

Constituição da RAM do Arduino
Constituição da RAM do Arduino (Fonte: Adafruit)

Static Data → É onde ficam armazenadas as variáveis globais e estáticas e seu tamanho não varia.

Stack → Memória de tamanho variável ocupada por Ponteiros para chamadas de funções e interrupções, variáveis locais, etc.

Heap → Alocação dinâmica de variáveis, principalmente variáveis de instância.

Os problemas acontecem porque o heap e o Stack podem crescer e ocupar a memória livre até o momento em que ela se esgota, causando erros imprevisíveis e comportamento errático.

Qual a memória ocupada?

Na finalização do processo de buid do Arduino, ele exibe a memória ocupada pelo sketch e pelas variáveis globais.

No entanto se você quer uma relatório mais detalhado sobre a ocupação da memória, existe um utilitário que acompanha a IDE chamado avr-size. Para usar essa ferramenta, você precisa compilar sua aplicação. Esse processo gera alguns arquivos intermediários com importantes informações na sua máquina que podem ser analisados pelo avr-size. 

O avr-size, fica na pasta tools, normalmente localizada em: C:\Arduino\hardware\tools\avr\bin

Veja aqui um tutorial completo para uso desse utilitário:
How to find out how much RAM your Arduino program uses

Dicas de Otimização

Vejamos agora algumas técnicas de programação e boas práticas que podem ser utilizadas com o objetivo de economizar memória em seus sketchs.

1) Global é do mal…

Embora a maioria dos sketchs que você vê por aí façam uso indiscriminado desse tipo de variável, essa prática não é aconselhada. As variáveis globais ficam armazenadas na seção static data da SRAM e ficam lá “eternamente” ocupando espaço.

Crie funções para as partes repetitivas do código. Programe orientado às funções. Prefira, sempre que possível, usar variáveis locais. Caso necessite que a variável seja acessada em vários pontos do programa, passe-as como parâmetro.

Devido à estrutura dos sketchs do Arduino, nem sempre é possível evitar o uso de variáveis globais. Mas é possível diminuir o seu uso.

Moral da História:
Pense globalmente. Aloque localmente!

2) Alocação dinâmica x estática

Variáveis locais são armazenados na Stack e seu espaço é liberado quando termina seu escopo. Já, as variáveis alocadas dinamicamente (malloc, calloc) ocupam o heap e nem sempre seu espaço é recuperado, podendo causar a fragmentação dessa região da memória.

Portanto:
Evite a alocação dinâmica da memória

3) Constantes, PROGMEM e a macro F()

O compilador GCC é inteligente, possui um otimizador embutido e sabe que pode armazenar as constantes na memória FLASH. Mas em alguns casos envolvendo constantes literais, isso nem sempre acontece. Por isso, é bom nos precavermos.

Os valores constantes são armazenados na memória FLASH, mas copiados para a memória SRAM ocupando precioso espaço com valores que nunca serão alterados.  O mesmo acontece com constantes Strings literais.

Para evitar esse desperdício de memória, podemos fazer uso da diretiva PROGMEM e a macro F()

PROGMEM é um modificador que instrui ao compilador a armazenar a constante na memória flash. O mesmo pode ser feito com Strings literais, através da macro F().

Veja alguns exemplos:

const char STR_SRAM[] = "Vou ocupar espaço na SRAM";// 26  bytes desperdiçados
...
lcd.print("Vou ocupar espaço na SRAM"); // 26  bytes desperdiçados
...
const PROGMEM char STR_FLASH[] = "Vou ocupar espaço na FLASH";// 26  bytes economizados
...
lcd.print(F("Vou ocupar espaço na FLASH")); // 26  bytes economizados

O que fica:
Com certeza, faça uso de PROGMEM e F() para suas constantes.

4) const ou define?

As constantes podem ser criadas no Arduino através da diretiva define ou do qualificador const.

Usar const é como definir uma variável, já que trata-se de um qualificador. Portanto, de fato, será alocado um espaço em memória para essa constante.

Já, o define,  na verdade, não faz parte da linguagem C. É uma etapa do processo de build que ocorre antes da compilação. Equivale a substituir no código-fonte todas as ocorrências da constante pelo valor definido.

Prefira usar o const, principalmente por motivos de depuração dos erros.

5) Variáveis String

O espaço ocupado pelas variáveis da classe String é alocado dinamicamente no heap, o que pode causar sua fragmentação devido principalmente às operações de concatenação. É possível evitar isso, reservando o espaço antes de executar as operações.

Exemplo:

String StarTrek;
StarTrek.reserve(50); //Este comando evitará a desfragmentação 
...
StarTrek = "Espaço, a fronteira final";
StarTrek += "Esta são as viagens da nave estelar Enterprise...";
...

Resumo:
Reserve suas Strings. Mas cuidado para não superdimensionar!

6) Selecionando os Tipos de Dados

Estude os tipos de dados disponíveis na plataforma e escolha aquele que vai ocupar menos espaço.

Por exemplo:
Ao invés de fazer:
const int pino_sensor = 10;
Prefira:
const byte pino_sensor = 10;

Como vimos anteriormente, o tipo int pode ocupar 4 bytes na memória em alguns processadores. Para ter certeza quantos bytes uma variável vai ocupar, prefira usar os aliases definidos no arquivo stdint.h.

Por exemplo:
Para usar um int de 2 bytes, ao invés de fazer:
int valor;
Prefira:
int16_t valor;

Tudo é questão de avaliar as opções com critério. Outro exemplo:

A temperatura em graus Celcius detectada por um sensor pode ser representada com 3 tipos de dados diferentes:
signed byte ⇒ Para temperaturas inteiras entre -127 até 127 (1 byte na memória)
int ⇒ Para temperaturas inteiras entre -32,768 até 32,767 (2 bytes)
float ⇒ Para temperaturas mais precisas com decimais (4 bytes na memória)

Qual tipo se encaixa melhor nos seu projeto?

Em suma:
Seja criterioso na definição de variáveis

7) Medidas extremas

Em último caso, quando não há mais o que fazer, podem-se tomar algumas medidas desesperadas:

  • Armazenamento de variáveis na EEPROM
  • Eliminação do bootloader

Veja essas técnicas nas referências abaixo.

Conclusão

When Heap meets Stack. There is all the danger.
De acordo com o que vimos, para programarmos de forma eficiente na plataforma Arduino, é necessário estudar a estrutura dos dados, definir de forma criteriosa sua configuração e desenvolver o código de forma organizada.

Conhece mais alguma dica de economizar memória no Arduino? Comente aí!

Saiba mais…

2 comentários em “Arduino: Organização e Otimização da Memória”

  1. Muito boas as dicas que você deu. Vou salvar aqui para futuros estudos. Percebi hoje que declaro muita variável desnecessária asduhsaduhasd e só pesquisei opções para memória porque estou com um problema no envio de sinais IR RAW, devido o seu tamanho. Declarando, consigo deixar apenas dois comandos no código, o que é impossível para o projeto.
    Vou tentar dessa forma que você fala acima e volto para dar o feedback.

  2. Excelentes dicas!!!
    Eu estava com problemas de Stack em um sketch que tinha muitas strings e saídas para a porta serial.
    O uso da memória dinâmica caiu de cerca de 1700 bytes para pouco mais de 440 bytes.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *