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