Fazer a gravação de certos dados pode ser complicado pela falta de um componente de memória, como um cartão SD. Entretanto, é possível utilizar a memória EEPROM do Arduino, já que ela já vem com o microcontrolador. Por conta disso, vamos aprender uma forma de gravar os dados na memória EEPROM e depois extraí-los para o computador em um arquivo Excel csv ou um arquivo txt.
É extremamente recomendável ler o post sobre como usar a memória EEPROM no Arduino, pois nele também explico sobre a estrutura da memória.
Ressalvas
Antes de iniciar o projeto, é importante fazer algumas observações:
Esse método é possivelmente o menos recomendado para este tipo de aplicação, pois a memória EEPROM tem uma vida útil relativamente pequeno e um, também, pequeno limite de espaço. Entretanto, é interessante aprender a fazê-lo, pois ele pode ser útil em casos onde você não tenha um módulo de cartão SD ou então não possa deixar o Arduino conectado ao computador para gravar direto para ele.
Se este não for o caso, recomendo a leitura deste post, sobre como salvar os dados em um arquivo txt, com o Arduino conectado ao computador.
Teoria do projeto
Quantidade e tamanho dos dados
A ideia do projeto é aproveitar ao máximo os 1024 bytes de dados contidos na memória EEPROM. Para isso, os valores lidos de um sensor serão remapeados para uma faixa de 0 a 255. Faremos isso, pois cada byte contém 8 bits, e 8 bits formam valores entre 0 e 255. Ao fazer esta escolha, ganhamos espaço para armazenar 1024 valores, mas teremos uma perda de informação no processo. Veja a imagem abaixo para entender a estrutura da memória EEPROM.
Se utilizássemos mais bytes para alocar tranquilamente os valores de um sensor (0 a 1023), ocuparíamos 2 bytes por dado (2 bytes formam números de 0 a 65535). Sendo assim, seria possível armazenar apenas 512 valores, o que é bem pouco.
Praticidade de medição
Para facilitar as medições, é interessante pensar em uma forma de gravar os dados continuamente sem que o RESET do Arduino atrapalhe. Para isso, cheguei a uma solução interessante:
Na posição 0 e 1 da memória da EEPROM, irei armazenar um valor que indicará em qual posição de gravação o programa estava por último. Desta forma, toda vez que o Arduino ligar, ele irá consultar este valor para saber onde deve continuar a gravar os dados. Isso torna o sistema bem prático, pois, mesmo desligando o Arduino, o sistema sabe de onde continuar. Com isso, nenhum dado será sobreposto acidentalmente.
Utilizarei as posições 0 e 1 da memória, pois preciso armazenar um valor que varie de 0 a 1023 e 1 byte apenas não é capaz de fazer isso. Com os dois primeiros bytes da memória, conseguirei armazenar valores de 0 a 65535. Veja a imagem abaixo para entender a ideia:
Além disso, quando a posição chegar no valor de 1023, irei voltá-la para o início (posição 2). Farei isso para a medição ser feita continuamente, até que a pessoa decida interromper o programa. A desvantagem disso é que os dados podem ser sobrepostos indevidamente. Mas bastará extrair os valores do Arduino de tempos em tempos para evitar este problema.
Tempo entre amostragens
Outra informação importante é o tempo entre a gravação de cada dado. Este tempo não pode ser pequeno, pois, assim, a quantidade de gravações por byte alcançaria a vida útil da EEPROM rapidamente (100.000 leituras/escritas).
Sendo assim, o ideal é ter um tempo considerável entre cada gravação. A fórmula, em função do tempo de amostragem, do tempo para alcançar o desgaste máximo da EEPROM está mostrada abaixo:
Todos os tempos são dados em segundos. 1022 é a quantidade máxima de bytes gravados, pois a posição 0 e 1 estão sendo usadas para outra função, conforme tópico anterior.
Basicamente, é só multiplicar a quantidade máxima de bytes gravados pelo tempo de amostragem (1022*Ta), e o resultado indica o tempo necessário para alcançar o mesmo byte novamente. Com este tempo, basta multiplicá-lo pela vida útil da EEPROM (100.000), que dará o tempo máximo até a memória chegar ao seu limite.
Repare que o tempo de amostragem de 1 segundo dá um tempo máximo de 102.200.000 segundos, que são aproximados 1182 dias. Ou seja, é muito tempo de vida útil.
Contudo, não podemos esquecer que as posições 0 e 1 armazenarão o tempo todo o valor da gravação, portanto o tempo de vida dela será 1022 vezes menor. Para contornar este problema, vamos gravar a posição a cada 15 gravações. Sendo assim, a fórmula para o tempo de vida máximo dos 2 primeiros bytes da memória é:
Como o valor é bem menor, devemos considerar um tempo de amostragem maior. Por exemplo, de 10 segundos (173 dias) ou um que esteja na ordem dos minutos.
Desenvolvimento
Obs.: O programa final contará com o uso do Processing (software) para fazer a gravação dos dados em um arquivo csv ou txt.
Circuito
O circuito que utilizarei é apenas um sensor de umidade ligado no pino analógico A0. Não há muito mistério, pois o sensor indica o nível de umidade de acordo com a tensão no pino de saída. Portanto, irei gravar estes valores de tensão.
Você pode utilizar qualquer sensor, ou qualquer valor de medição que desejar. Dependendo, terá apenas que fazer algumas pequenas mudanças na programação.
Programação do Arduino
Não vou entrar em todos os detalhes do código completo, apenas mostrar a lógica geral dele. Entretanto, coloquei comentários para facilitar o entendimento. E recomendo a leitura do post sobre EEPROM com Arduino, para entender bem os comandos que serão utilizados.
Posição inicial
Antes de começar a gravação, é importante gravar nos bytes 0 e 1 a posição inicial da leitura. Pois, a principio, estes dois bytes podem ter qualquer valor. E o código para isto está mostrado abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Biblioteca dos comandos de EEPROM #include <EEPROM.h> void setup() { // Variavel inteira (2 bytes - 16bits) para definir a posição inicial int posicao = 2; // Grava no endereço 0 e 1, pois a variavel int ocupa 2 bytes EEPROM.put(0, posicao); } void loop() { } |
Gravação
Para fazer a gravação, é importante criar uma variável para armazenar a posição que o programa deverá continuar a gravar os dados. Temos, então:
1 | int posicao; |
Em seguida, na hora que o Arduino inicializa (no setup), precisamos pegar o valor da posição que está armazenado nos bytes 0 e 1. Para isso, existe o comando EEPROM.get(), que pega o valor armazenado de acordo com o tipo de variável informada. Como a variável posição é uma int, ele irá pegar o valor de dois bytes, pois a variável int ocupa 2 bytes.
1 | posicao = EEPROM.get(0, posicao); |
No loop, devemos primeiro fazer a leitura do sensor e depois remapeá-lo para caber entre valores de 0 a 255 (1 byte). Os dois comandos para isto estão mostrados abaixo:
1 2 3 | int leitura = analogRead(A0); leitura = map(leitura, 0, 1023, 0, 255); |
O próximo passo é gravar o dado lido na posição atual:
1 | EEPROM.update(posicao, leitura); |
Além disso, devemos incrementar o valor da posicao (varia de 2 a 1023), e, caso ele tenha estourado (chegou em 1024), devemos voltá-lo à posição 2:
1 2 3 4 5 | posicao++; if(posicao >= EEPROM.length()){ posicao = 2; } |
Por fim, basta gravar o valor da variável posicao nos bytes 0 e 1 da EEPROM a cada 15 medições. Fiz isso verificando se a variável posicao é divisível por 15:
1 2 3 4 | if(posicao%15 == 0){ // Grava a nova posição na memória EEPROM.put(0, posicao); } |
E, claro, não devemos esquecer do intervalo entre as medições:
1 | delay(10000); |
Leitura pelo computador
A leitura é feita dentro do mesmo código da gravação.
No caso da leitura, a lógica que utilizei foi pegar os dados gravados e enviá-los para o Processing via comunicação serial. Para isso, o programa verifica, no void setup, se alguma informação foi recebida pelo serial. Se sim, ele envia todos os dados gravados na memória EEPROM e então fica travado sem fazer nada. Senão, ele apenas continua a logica de gravação mostrada no tópico anterior.
O primeiro passo é criar uma variável booleana para indicar se o programa deve ficar travado ou não, caso tenha entrado no modo de leitura. Ela começa em false, pois é o estado padrão do programa.
1 | bool leitura = false; |
Toda a lógica a seguir é feita no void setup.
Em seguida, devemos iniciar a comunicação serial:
1 | Serial.begin(9600); |
Após isto, o programa deve ficar verificando por um pequeno período se algo foi recebido ou não na comunicação serial. Para evitar o uso de delays e atrapalhar o funcionamento do programa, coloquei a verificação dentro de um for que vai de 0 a 5000. O for garante que o programa fique ali por um tempo pequeno (indeterminado), mas também garante que não fique preso dentro de um delay.
1 2 3 4 5 6 7 | for(unsigned int i = 0; i <= 5000; i++){ if(Serial.available() > 0){ // Envia os dados } } |
Para a parte de enviar os dados, bastou criar outro for variando de 2 a 1023 e enviar os valores dos bytes da EEPROM pela comunicação serial. E também foi necessário ativar o modo leitura e utilizar o comando break para sair do for anterior.
1 2 3 4 5 6 7 | for (int j = 2; j < 1024;j++){ Serial.println(EEPROM.read(j)); } // Ativa o modo leitura leitura = true; break; |
Por fim, depois dos comandos acima, bastou criar um while para fazer o programa ficar travado no setup caso o modo leitura estivesse ativo. Isso impede o programa de continuar sua operação.
1 | while (leitura); |
Código completo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // Biblioteca dos comandos de EEPROM #include <EEPROM.h> // Variavel para saber em qual posição de memoria os dados devem ser gravados int posicao; // Variavel para saber se o programa está no modo leitura ou não bool leitura = false; void setup() { // Verifica qual a ultima posição que o sensor estava lendo os dados posicao = EEPROM.get(0, posicao); // Inicia a comunicação serial Serial.begin(9600); // Fica verificando por um tempo se o Processing deseja ler os valores for(unsigned int i = 0; i <= 5000; i++){ if(Serial.available() > 0){ for (int j = 2; j < 1024;j++){ Serial.println(EEPROM.read(j)); } // Ativa o modo leitura leitura = true; break; } } // Se estiver no modo leitura, o programa fica travado neste while while (leitura); // Delay antes do programa entrar na gravação dos dados delay(1000); } void loop() { // Faz a leitura do sensor int leitura = analogRead(A0); // Remapeia os valores do sensor para caber entre 0 e 255 (1 byte) leitura = map(leitura, 0, 1023, 0, 255); // Grava a leitura na memoria EEPROM.update(posicao, leitura); // Atualiza a próxima posição do endereço posicao++; // Se a posição chegou no limite (1024 para o Arduino UNO) if(posicao >= EEPROM.length()){ // Volta a posição para o endereço 2 (2 bytes depois dos bytes de posição); posicao = 2; } // Se a posição for multiplo de 15 if(posicao%15 == 0){ // Grava a nova posição na memória EEPROM.put(0, posicao); } // Delay entre cada leitura delay(10000); } |
Programação do Processing
Observações
Não pretendo explicar o código, pois os comentários já devem ser suficientes. Entretanto, algumas observações devem ser feitas:
Na linha abaixo, mude o valor entre os [] para a porta serial que está conectado seu Arduino. A numeração começa em 0 e varia de acordo com a ordem das portas conectadas no seu computador. Ex: se a porta COM1 e COM4 estiverem conectadas, a porta COM4 será a [1]. Para saber qual número colocar, fui na lista de portas da IDE do Arduino e vi em qual ordem estava a porta que eu estava usando.
1 | String portName = Serial.list()[1]; // Pega qual é a porta serial |
Outro detalhe é o comando da linha abaixo, que cria o arquivo. Para mudar entre criar um arquivo csv ou um txt, basta mudar a parte que está entre aspas. Portanto, se desejo um arquivo txt, a mudança ficaria: “data.txt”.
1 | output = createWriter( "data.csv" ); |
Código completo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // Biblioteca para comunicação serial import processing.serial.*; // Cria uma instancia da comunicação serial Serial mySerial; // Cria uma instancia do manipulador de arquivo PrintWriter output; // Funcionalidade de escrita em arquivos // Variavel para saber quantos dados foram lidos int leituras = 0; void setup() { String portName = Serial.list()[1]; // Pega qual é a porta serial mySerial = new Serial(this, portName, 9600); // Cria a comunicação serial a porta serial output = createWriter( "data.csv" ); // Cria o arquivo } void draw() { // Mesma coisa que a funçao loop do Arduino // Se receber um valor na porta serial if (mySerial.available() > 0 ) { // Se for a primeira leitura do programa, cria um aviso indicando o início da gravação if(leituras == 0){ println("Começou"); } // Le o valor recebido String dado = mySerial.readStringUntil('\n'); output.print(dado); // Escreve o valor lido no arquivo leituras++; // Incrementa a variavel leitura }else{ // Se não receber nada no serial, fica enviando uma mensagem até o Arduino captar e enviar os dados da memória de volta mySerial.write("Pronto!"); } // Se já deu 1022 leituras, cria um aviso e para o programa if(leituras >= 1022){ print("Leitura terminada"); output.flush(); // Termina de escrever os dados pro arquivo output.close(); // Fecha o arquivo exit(); // Para o programa } } void keyPressed() { //Se alguma tecla for pressionada output.flush(); // Termina de escrever os dados pro arquivo output.close(); // Fecha o arquivo exit(); // Para o programa } |
Resultados
Lembre-se de rodar o código do Arduino de definir a posição inicial da memória EEPROM, caso contrário as gravações começarão a partir de um ponto indeterminado.
Para testar, deixei o Arduino fazendo a medição por um pequeno tempo, e depois liguei ele no computador (com o mesmo circuito montado). Abri o Processing e rodei o programa para extrair os dados. Com os dados no arquivo csv, criei um gráfico para facilitar a exibição, que pode ser visto abaixo:
A primeira parte do gráfico (a reta horizontal) foi a parte que gravei os dados de verdade (de 0 a 71). Os valores restantes são valores antigos que estavam armazenados na memória. Eles foram obtidos de um testes antigo que tinha feito, por isso este formato estranho de rampa.
Por fim, para mostrar o que acontece quando o programa chega na posição 1023, fiz um teste com a posição começando em 1020. Veja o resultado abaixo mostrando o valor da variável posicao e o valor lido em A0.
Portanto, o programa basicamente retorna à posição 2 e continua a gravação dos dados.
Com tudo o que foi mostrado, você é capaz de criar um sistema para gravar dados sem a necessidade de conectar o Arduino ao computador. Lembre-se que você pode modificar o código que fiz para ficar adequado à sua aplicação.
Massa fera!
Valeu!
Muito bem explicado! Parabéns pelo trabalho profissional e de excelente qualidade!
Eu é que agradeço pelo comentário, Giovanni! Espero que você goste dos demais conteúdos do site.