Dicas para redução da utilização da memória - Técnicas de otimização em sketches Arduino


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 performance e estabilidade.
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 variáveis, tipos de dados e organização da memória na plataforma Arduino.

Tipos de Dados

Os tipos de dados primitivos usados pela plataforma podem ser resumidos na seguinte tabela:

Tipos de Dados no Arduino (Fonte: Adafruit)


Observemos que:

  • 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 possuem a mesma precisão e ocupam o mesmo espaço na memória;
  • int e short são equivalentes.


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 o modificador static.


Tipos de 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 (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.

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.

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()

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("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) 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!

5) 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:
int idade = 18;
Prefira:
byte idade = 18;

Outro ponto é evitar criar variáveis indiscriminadamente. Verifique se todas as variáveis estão sendo usadas.

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

6) 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...

Exibições: 6024

Comentar

Você precisa ser um membro de Laboratorio de Garagem (arduino, eletrônica, robotica, hacking) para adicionar comentários!

Entrar em Laboratorio de Garagem (arduino, eletrônica, robotica, hacking)

Comentário de Rodrigo Corbera em 20 outubro 2020 às 0:46

Luciano,

#define TEMPO 500  // essa linha não declara qualquer variável e nem aloca espaço na RAM e nem em FLASH

abaixo declaramos uma variável chamada RamVar com o valor 500 (uma mera subtituição, como se fosse um localize->subtitua

int RamVar = TEMPO;  

abaixo declaramos uma variável que alocará FLASH, usando antes da declaração do tipo e nome da variável o modificador de tipo "const PROGMEM" - isso é o que faz a variável ficar em Flash!

const PROGMEM int FlashVar = TEMPO/50;   // FlashVar terá o valor constante de 10

TEMPO é substituido por 500 em todo lugar ANTES de iniciar a compilação do programa por um processo chamado de PRE-PROCESSADOR.

poderíamos fazer algo como:

#define LARGURA 200

#define CENTRO (LARGURA/2)

int x = CENTRO; // o preprocessador vai subtituir esta linha para int x = 100;

x será uma variável alocada em RAM com 2 bytes por causa do espaço necessário para um tipo <int> -- isso para o processador ATMEL 328.... outros processadores de 32 bits vão alocar 4 bytes para o tipo <int>

Espero que tenha ajudado a entender o que faz o #define e como declarar variáveis em RAM ou variáveis constantes em FLASH, para a qual o seu valor NUNCA muda ao longo da execução do programa.

Abraços.

Comentário de Luciano Henrique Albano em 19 outubro 2020 às 23:34

Alguém poderia confirmar como é o comportamento das variáveis que são declaradas utilizando o #define. Elas ficam na flash ou vão para a Ram desperdiçando recursos?

Comentário de Akira Sato em 7 novembro 2016 às 21:53

muito bom, bem escrito

Comentário de Carlos Eduardo Freitas em 2 setembro 2016 às 12:02

Muito boa a explicação.

Só ajuste o exemplo da utilização da macro F() que a mesma não foi adicionada.

lcd.print("Vou ocupar espaço na FLASH"); // 26  bytes economizados

para

lcd.print(F("Vou ocupar espaço na FLASH")); // 26  bytes economizados

Parabéns
Comentário de Alexandre em 2 junho 2016 às 12:24

Excelente!

Comentário de Jose Augusto Cintra em 23 maio 2016 às 10:06

Flavio, bom dia!

Realmente usar define para constantes é melhor do que usar variáveis. Deixa o código organizado. Outra alternativa é usar const. A vantagem é que const é tipado.

Existe uma polêmica em torno de define x const:

http://www.revista-programar.info/artigos/arduino-const-vs-define/
http://forum.arduino.cc/index.php?topic=44023.0
https://www.arduino.cc/en/Reference/Define

Comentário de Flavio Hernan em 22 maio 2016 às 20:22

Usar o pre-processador para atribuir valores fixos (constantes), ou definição de pinos, em vez de usar int pino 13, por exemplo.

#define PINO13 13

 ...

pinMode(PINO13, OUTPUT);

...

digitalWrite(PINO13, HIGH);
...
digitalWrite(PINO13, LOW); 

Comentário de Marcelo Rodrigues em 22 maio 2016 às 16:18

Excelente!!! 

© 2024   Criado por Marcelo Rodrigues.   Ativado por

Badges  |  Relatar um incidente  |  Termos de serviço