Armazenando dados de forma não volátil no Arduino – EEPROM

A necessidade de se armazenar informações é algo bastante recorrente no desenvolvimento de projetos embarcados. Sejam elas um conjunto de variáveis para configurar o seu sistema, ou até  mesmos dados que você queira visualizar após um intervalo de tempo, o armazenamento de informações é muito importante em praticamente todos os tipos de projetos. Porém nem sempre dispomos de um cartão SD, seja por limitações de custo ou até mesmo pinos de I/O, impossibilitando  assim que a utilização de um cartão SD resolvesse o problema. Sabendo disso, neste tutorial, você aprenderá como utilizar a memória EEPROM existente nos microcontroladores Atmega das placas Arduino para armazenar dados permanentemente.

[toc]

Memória EEPROM

A memória EEPROM ou Electrically-Erasable Programmable Read-Only Memory, consiste em um modelo de memória onde diferente da memória RAM, podemos manter dados armazenados após desligarmos o nosso equipamento. Desta forma, é possível salvar informações que podem ser necessárias para o funcionamento do sistema após o seu desligamento, como por exemplo:

  • Configurações
  • Dados de difícil obtenção
  • Dados  estáticos

A utilização de memórias EEPROM, possibilitam com que o desenvolvedor seja capaz de armazenar diversas informações do sistema para uso posterior. Porém, este tipo de  memória possui um ciclo muito pequeno de escritas, o que torna o seu uso viável apenas para o armazenamento de informações que serão pouco modificadas.

Memória eeprom i2c com encapsulamento dip.

O processo de leitura e escrita neste tipo  de memória pode ser feito de duas formas:

  • Paralelo: Cada byte é escrito de forma paralela na memória, sendo  este escrito com base no endereço definido no barramento. Mais rápida, porém gasta uma maior quantidade de pinos para I/O

  • Serial: Cada byte é escrito de forma serializada (normalmente utilizando o protocolo i2c) na memória eeprom. Mais lenta, porém gasta uma menor quantidade de pinos para I/O

Memória EEPROM no Arduino

Os microcontroladores ATmega possuem em sua arquitetura uma pequena memória eeprom, que pode ser utilizada como uma unidade de armazenamento. Cada microcontrolador possui uma de tamanho específico,  segmentada em 1 byte por endereço. A lista abaixo ilustra a quantidade de memória disponível para a microcontroladores utilizados em plataformas como Arduino Nano, Arduino Uno e Arduino Mega:

  • ATmega8,ATmega168 – 512 Bytes
  • ATmega326 – 1024 Bytes
  • ATmega1280, ATmega2560 – 4096 Bytes

Mãos à obra – Armazenando dados na memória EEPROM do Arduino

Neste projeto iremos apenas aprender a como manipular e realizar leituras e escritas na memória EEPROM do Arduino, desta forma iremos precisar apenas de um Arduino com algum dos microcontroladores citados acima.

Componentes Utilizados

Neste projeto iremos utilizar um Arduino Nano, porém praticamente todos os  microcontroladores ATmega dispõem de uma memória eeprom nativa.

Programando

– Bibliotecas

Como iremos apenas aprender como armazenar e ler dados da memória EEPROM do Arduíno, iremos utilizar apenas a biblioteca EEPROM.h que já é nativa da ide que pode ser importada ao seu código da seguinte forma:

#include <EEPROM.h>

– Código Utilizado

Agora que temos o nosso sistema montado, e as bibliotecas adicionadas ao projeto, podemos partir para o código. Observem o código a seguir que utilizaremos como base para o nosso manipulador de memória.

#include <EEPROM.h> // Biblioteca para acesso e manipulação da EEPROM
void setup()   
{
  Serial.begin(115200); // Inicialização da comunicação serial

  Serial.print("Espaco Disponivel em Bytes: "); // Mostra a quantidade  de memória da EEPROM do seu microcontrolador
  Serial.println(EEPROM.length()); // Mostra a quantidade de memória da EEPROM do seu microcontrolador
  escreveByte(0,254); // Escreve o valor 255 na posição 0 da memória  
  byte valor = leByte(0); // Lê o endereço 0 da memória ( onde escrevemos 255 )
  Serial.print("Byte Armazenado: "); // Mostra o Byte Armazenado
  Serial.println(valor); // Mostra o Byte Armazenado
  escreveInt(1,2,1200); // Escreve o valor 1200 na EEPROM ( por ser um int de 2 bytes precisamos utilizar 2 endereços para armazenar)
  Serial.print ("Inteiro Armazenado: "); // Mostra o inteiro Armazenado
  Serial.println(lerInt(1,2)); // Mostra o inteiro Armazenado
  escreveString(3,"Vida de Silício"); // Escreve a String Vida de Silício na EEPROM, começando no  endereço 3
  Serial.println("String Armazenada: "+leString(3)); // Mostra a String armazenada
}

void loop(){
  
}

byte leByte (int endereco1){
  return EEPROM.read(endereco1); // Realizamosa leitura de 1 byte  e retornamos
}

void escreveByte (int endereco1, byte valor){ // Escreve um byte na EEPROM no endereço especificado
  byte valorAtual = leByte(endereco1); // Lemos o byte que desejamos escrever
  if (valorAtual == valor){ // Se os valores forem iguais não  precisamos escrever ( economia de ciclos de escrita )
    return;
  }
  else { // Senão escrevemos o byte no endereço especificado na função
    EEPROM.write(endereco1,valor); // Escreve o byte no endereço especificado na função
  }
  
}



void escreveInt(int endereco1, int endereco2, int valor){ // Escreve um inteiro de 2 bytes na EEPROM
  int valorAtual = lerInt(endereco1,endereco2); // Lemos o valor inteiro da memória
  if (valorAtual == valor){ // Se o valor lido for igual ao que queremos escrever não é necessário escrever novamente
    return;
  }
  else{ // Caso contrário "quebramos nosso inteiro em 2 bytes e escrevemos cada byte em uma posição da memória
      byte primeiroByte = valor&0xff; //Executamos a operação AND de 255 com todo o valor, o que mantém apenas o primeiro byte
      byte segundoByte = (valor >> 8) &0xff; // Realizamos um deslocamento de 8 bits para a direita e novamente executamos um AND com o valor 255, o que retorna apenas o byte desejado
      EEPROM.write(endereco1,primeiroByte); // Copiamos o primeiro byte para o endereço 1
      EEPROM.write(endereco2,segundoByte); // Copiamos o segundo byte para o endereço 2
  }
}

int lerInt(int endereco1, int endereco2){ // Le o int armazenado em dois endereços de memória
  int valor = 0; // Inicializamos nosso retorno
  byte primeiroByte = EEPROM.read(endereco1); // Leitura do primeiro byte armazenado no endereço 1
  byte segundoByte = EEPROM.read(endereco2); // Leitura do segundo byte armazenado no endereço 2
  valor = (segundoByte << 8) + primeiroByte; // Deslocamos o segundo byte 8 vezes para a esquerda ( formando o byte mais significativo ) e realizamos a soma com o primeiro byte ( menos significativo )
  return valor; // Retornamos o valor da leitura

}

void escreveString(int enderecoBase, String mensagem){ // Salva a string nos endereços de forma sequencial
  if (mensagem.length()>EEPROM.length() || (enderecoBase+mensagem.length()) >EEPROM.length() ){ // verificamos se a string cabe na memória a partir do endereço desejado
    Serial.println ("A sua String não cabe na EEPROM"); // Caso não caiba mensagem de erro é mostrada
  }
  else{ // Caso seja possível armazenar 
    for (int i = 0; i<mensagem.length(); i++){ 
       EEPROM.write(enderecoBase,mensagem[i]); // Escrevemos cada byte da string de forma sequencial na memória
       enderecoBase++; // Deslocamos endereço base em uma posição a cada byte salvo
    }
    EEPROM.write(enderecoBase,'\0'); // Salvamos marcador de fim da string 
  }
}

String leString(int enderecoBase){
  String mensagem="";
  if (enderecoBase>EEPROM.length()){ // Se o endereço base for maior que o espaço de endereçamento da EEPROM retornamos uma string vazia
    return mensagem;
  }
  else { // Caso contrário, lemos byte a byte de cada endereço e montamos uma nova String
    char pos;
    do{
      pos = EEPROM.read(enderecoBase); // Leitura do byte com base na posição atual
      enderecoBase++; // A cada leitura incrementamos a posição a ser lida
      mensagem = mensagem + pos; // Montamos string de saídaa
    }
    while (pos != '\0'); // Fazemos isso até encontrar o marcador de fim de string
  }
  return mensagem; // Retorno da mensagem
}


Colocando pra funcionar

Se nada tiver sido modificado no código e sua EEPROM ainda possuir ciclos de escrita, a seguinte mensagem será apresentada no monitor serial:


Entendendo a Fundo

Software

– Incluindo bibliotecas necessárias

Para este sistema, iremos precisar apenas da biblioteca EEPROM.h que já é  nativa do Arduíno e será utilizada como interface entre a EEPROM e o nosso código, desta forma ela pode ser adicionada  da seguinte forma:

#include <EEPROM.h>

– Função Setup

Em nossa função setup, iremos basicamente escrever e ler as variáveis que desejamos armazenar na EEPROM, foram definidos um conjunto de 6 funções, sendo três para escrita e três para leitura de tipos de dados normalmente utilizados, sendo eles:

  • Int
  • Byte
  • String

Sendo assim, no setup iremos apenas utilizar estas funções, da seguinte forma:

– Função escreve byte

Escreve um valor em byte no endereço especificado, neste exemplo estamos escrevendo o valor 254 no endereço 0 da memória.

  escreveByte(0,254); // Escreve o valor 255 na posição 0 da memória

– Função lê byte

Realiza a leitura de um único byte no endereço especificado pelo usuário, neste exemplo estamos lendo o endereço 0.

byte valor = leByte(0);

– Função escreve int

Escreve um valor inteiro composto por 2 bytes, em duas posições quaisquer da eeprom, neste exemplo estamos escrevendo o valor 1200 utilizando os endereços 1 e 2.

escreveInt(1,2,1200);

– Função lê int

Realiza a leitura de dois endereços especificados pelo usuário, reconstruindo o valor inteiro que elas representam, neste exemplo estamos lendo os endereços 1 e 2 da memória.

Serial.println(lerInt(1,2));

– Função escreve string

Escreve uma determinada string de forma sequencial na EEPROM, neste exemplo estamos utilizando um total de 15 endereços (começando do endereço 3) para escrever a string “Vida de Silício” em nossa memória.

escreveString(3,"Vida de Silício");

– Função lê string

Dado um endereço base fornecido, esta função lê sequencialmente toda a string armazenada.

leString(3);

Entendendo cada função implementada

Cada função implementada possui um grau complexidade devido a realização de operações lógicas para quebra de bytes e armazenamento sequencial, desta forma iremos mostrar a forma como cada função é capaz de salvar e recuperar seus respectivos tipos de dados. Porém antes vamos entender um pouco a forma como a nossa memoria funciona.

Imagine a sua  memória como um um conjunto de blocos onde podemos armazenar valores em um intervalo de 0 até 255, e cada um desses intervalos está associado a um endereço, sendo assim, podemos imaginar a nossa memória da seguinte forma: Com essa estrutura de memória podemos salvar qualquer tipo de informação, desde  que o espaço seja suficientemente grande para armazenar todos os valores.

– Função escreveByte

A função escreveByte recebe como argumentos o endereço  e o valor a ser escrito naquela  posição, verificando apenas se o byte que desejamos escrever já não está lá, desta forma economizamos uma escrita que seria feita desnecessariamente, prologando assim a vida útil de nossa memória.

void escreveByte (int endereco1, byte valor){
  byte valorAtual = leByte(endereco1);
  if (valorAtual == valor){
    return;
  }
  else {
    EEPROM.write(endereco1,valor);
  }
}

Estruturalmente, podemos dizer que esta função executa a seguinte operação:

– Função leByte

Já a função leByte, basicamente realiza a leitura do endereço especificado em seu argumento, retornando assim o valor que está armazenado naquele endereço.

byte leByte (int endereco1){
  return EEPROM.read(endereco1);
}

Utilizando o nosso modelo de memória, podemos dizer que estamos realizando a seguinte operação:

– Função escreveInt

Nos microcontroladores ATmega, uma variável  do tipo int, é representada por um total de 2 bytes, sendo estes divididos entre mais significativos (HB) e menos significativos (LB), como a nossa memória permite apenas o armazenamento de 1 byte por endereço. Sabendo disso, é necessário que a nossa função seja capaz de “quebrar” o nosso  valor inteiro de 2 bytes, em duas variáveis de 1 byte cada, para que assim seja possível armazenar esta informação na memória. Para que isso seja feito, iremos utilizar dois tipos de operações lógicas existente em praticamente todas as arquiteturas de computadores, que são as operações rigth shift e and bitwise.

void escreveInt(int endereco1, int endereco2, int valor){
  int valorAtual = lerInt(endereco1,endereco2);
  if (valorAtual == valor){
    return;
  }
  else{
      byte primeiroByte = valor&0xff;
      byte segundoByte = (valor >> 8) &0xff;
      EEPROM.write(endereco1,primeiroByte);
      EEPROM.write(endereco2,segundoByte);
  }
}

 

Sabendo disso o algoritmo funciona da seguinte forma:

  1. Inicialmente verificamos se o valor que desejamos inserir é igual ao que está na memória ( se for economizamos duas escritas na memória )
  2. Caso não seja executamos a seguinte operação:
    1. Dado o nosso valor de exemplo 1001, serão executadas as seguintes operações:
    2.  Aplicamos uma operação AND bit a bit sob o valor que desejamos quebrar com o número 255, desta forma iremos preservar apenas os bits da primeira cadeia de byte que desejamos armazenar. A figura abaixo ilustra como o processo é feito e o resultado obtido através da operação realizada.
    3. Para o segundo byte, realizaremos o mesmo processo do passo 2, porém ao invés apenas aplicar a operação lógica AND, iremos antes deslocar os bits que estão na camada superior. Para que dessa forma, cheguem a posição dos bits menos significativos para a aplicação da  máscara.
    4.  Por fim, com os bytes separados, a  função salva cada um deles no endereço especificado como mostra a figura abaixo:

– Função leInt

O processo de leitura segue praticamente a lógica inversa do processo de escrita, onde iremos ler cada byte e montar o nosso valor da seguinte forma:

  1. Relizamos uma leitura do endereço de valor mais significativo, este  valor é deslocado 8 bits a esquerda para que volte a sua posição de bit mais significativo.
  2. Realizamos uma leitura do endereço de valor menos significativo, este valor é somado ao  valor deslocado para que volte a posição de bit menos significativo.
  3. Temos o nosso  valor montado novamente.

 

int lerInt(int endereco1, int endereco2){
  int valor = 0;
  byte primeiroByte = EEPROM.read(endereco1);
  byte segundoByte = EEPROM.read(endereco2);
  valor = (segundoByte << 8) + primeiroByte;
  return valor;
}

– Função escreveString

A função escreve string por sua vez, não utiliza de recursos de deslocamento de bits, devido ao fato de que cada caractere, é um único byte. Ou seja, uma string nada mais é do que um vetor de bytes alocados sequencialmente. Sabendo disso, o nosso algoritmo de salvar uma string na memória segue a seguinte lógica:

  1. Dado um endereço base, e o tamanho da string verificamos se é possível o armazenamento ou não.
    1. Caso não seja possível, o  valor não é salvo.
    2. Caso seja possível, a string é alocada sequencialmente na memória começando com o endereço base e terminando sempre com um ”\0”
void escreveString(int enderecoBase, String mensagem){
  if (mensagem.length()>EEPROM.length() || (enderecoBase+mensagem.length()) >EEPROM.length() ){
    Serial.println ("A sua String não cabe na EEPROM");
  }
  else{
    for (int i = 0; i<mensagem.length(); i++){
       EEPROM.write(enderecoBase,mensagem[i]);
       enderecoBase++;
    }
    EEPROM.write(enderecoBase,'\0');
  }
}

– Função leString

A função leString por sua vez, realiza praticamente o mesmo processo que a leitura, porém ao invés de armazenar os bytes como na função escreveString, ela utiliza a função read, para ler cada byte armazenado sequencialmente. Desta forma podemos dizer que o algoritmo funciona da seguinte forma:

  • Com base no endereço inicial, a função lê cada byte lido, até que o caractere de fim de texto ‘\0’, seja encontrado. Quando encontrado uma string montada é retornada pela função.
String leString(int enderecoBase){
  String mensagem="";
  if (enderecoBase>EEPROM.length()){
    return mensagem;
  }
  else {
    char pos;
    do{
      pos = EEPROM.read(enderecoBase);
      enderecoBase++;
      mensagem = mensagem + pos;
    }
    while (pos != '\0');
  }
  return mensagem;
}

Desafio

Agora que sabemos como manipular a memória EEPROM do nosso Arduino, tente adicionar esta funcionalidade a algum projeto que seja necessária a configuração de uma variável em tempo real.  Um exemplo de sistema desse tipo são os dataloggers, onde  precisamos configurar informações como tempo de leitura e configuração de sensores.

Considerações finais

Este tutorial, teve como objetivo mostrar como manipular a memória EEPROM, interna do seu Arduino, funcionando como uma pequena unidade de armazenamento para informações importantes. Espero que tenham gostado do conteúdo apresentado, sinta-se à vontade para nos dar sugestões, críticas ou elogios. Lembre-se de deixar suas dúvidas nos comentários abaixo.

Privacy Preference Center