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:
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í!
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.
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.