Pular para o conteúdo
IDE livre

Armazenamento

Guardar dados na ESP32: arquivos, NVS, memória RTC, SD e EEPROM

Onde guardar dados na ESP32 com MicroPython: RAM, arquivos no LittleFS, NVS, memória RTC, cartão SD, EEPROM e FRAM, desgaste da flash e gravação em lotes.

Por Prof. Rodrigo Faustino · Atualizado em · 11 min de leitura

Métricas deste artigo

Palavras
2.094
Caracteres
12.016
Tempo de leitura
~11 min

Um termômetro que mostra a temperatura agora é útil; um que guarda a temperatura de cada hora da semana é muito mais. Uma estação meteorológica que esquece o nome da rede Wi-Fi toda vez que falta luz irrita qualquer um. Quase todo projeto com a ESP32 acaba precisando guardar dados — e a primeira surpresa é que existem vários lugares para isso, cada um com regras diferentes sobre o que sobrevive a desligar, quanto cabe e quantas vezes se pode gravar.

Este artigo percorre esses lugares no MicroPython: a RAM, os arquivos na memória flash, a NVS para configurações, a memória RTC que atravessa o sono profundo e os componentes externos — cartão microSD, EEPROM e FRAM. No caminho, aparecem os cuidados que fazem um registro de dados durar: o desgaste da flash, a gravação em lotes, a rotação de arquivos e os formatos que economizam espaço.

Tabela com sete lugares para guardar dados na ESP32 e três colunas: se sobrevive a desligar, quanto cabe e quantas gravações aguenta. RAM: não sobrevive, cerca de 160 KB de heap. Memória RTC: sobrevive ao deep sleep e ao reset suave, 2048 bytes. NVS e arquivos no LittleFS: sobrevivem, na flash, com cerca de 100 mil gravações por setor; os arquivos têm 2 MiB na DevKit V1. Cartão microSD: gigabytes. EEPROM AT24C32: 4 KB e 1 milhão de gravações. FRAM MB85RC256V: 32 KB e 10 elevado a 12 gravações por byte.
Cada memória tem um papel: a RAM é rápida e some ao desligar, a flash guarda arquivos e configurações, e os chips externos ajudam quando é preciso gravar muito ou guardar muito.

RAM: rápida e esquecida

Tudo o que o programa guarda em variáveis — números, listas, dicionários, textos — fica na RAM. Ela é rápida e pode ser alterada quantas vezes se quiser, mas apaga ao desligar e também a cada reset. Na ESP32 DevKit V1 com o MicroPython v1.29.0, o espaço para os objetos do programa (o heap) foi medido logo depois de ligar: gc.mem_free() devolveu 157 632 bytes livres, de um heap de 166 624. Cerca de 150 KB parece muito, mas some rápido com listas grandes de leituras ou textos longos — e por isso a RAM serve de lugar de passagem: junta leituras por um tempo e depois manda tudo para um lugar que dura.

FastTeacherPara professores

Da ementa ao semestre planejado

  • Plano de ensino e calendário montados sozinhos
  • Diário de aulas pronto para colar no SIGA
  • Slides e listas de exercícios com gabarito
Conhecer o FastTeacher

Versão Community grátis · Windows

Arquivos na flash: o LittleFS

A ESP32 guarda o firmware do MicroPython numa memória flash soldada na placa, e o que sobra dela vira um sistema de arquivos. É nele que estão o main.py e o boot.py, e é nele que o programa pode criar os próprios arquivos com o open() do Python. Na ESP32, o MicroPython monta nesse espaço o LittleFS, um sistema de arquivos feito para memórias flash. Na DevKit V1 com o firmware v1.29.0, os.statvfs('/') mostra 512 blocos de 4096 bytes, ou 2 MiB.

O LittleFS tem duas qualidades que importam muito em projetos com bateria e sensores: foi projetado para resistir a quedas de energia no meio de uma gravação, e espalha as gravações pelos blocos da flash (o chamado nivelamento de desgaste). A documentação do MicroPython lembra que o sistema FAT, a outra opção do firmware, não tolera bem faltar energia durante uma escrita.

Configuração em JSON

O uso mais comum dos arquivos é a configuração: o nome da rede, o intervalo entre leituras, o limite do alarme. O formato JSON casa bem com os dicionários do Python. O exemplo abaixo roda no simulador, que tem um sistema de arquivos de verdade com as regras do LittleFS: ele tenta ler o config.json, usa valores padrão se o arquivo não existe ou está estragado, muda um valor e grava de volta.

import json
import os

PADRAO = {'local': 'Laboratório', 'intervalo_s': 60, 'limite_c': 30.0}

def carregar():
    try:
        with open('config.json') as f:
            salvo = json.load(f)
    except (OSError, ValueError):
        return dict(PADRAO)          # não existe ou não é JSON válido
    config = dict(PADRAO)
    config.update(salvo)             # chaves novas ganham o valor padrão
    return config

def salvar(config):
    with open('config.json', 'w') as f:
        json.dump(config, f)

config = carregar()
print('antes:', config)
config['limite_c'] = 28.5
salvar(config)
print('depois:', carregar())
print('arquivos:', os.listdir())
print('tamanho do config.json:', os.stat('config.json')[6], 'bytes')

Três detalhes desse exemplo valem para qualquer programa. O with fecha o arquivo ao sair do bloco, e só então os dados vão de fato para a flash; um arquivo esquecido aberto pode perder o que foi escrito se a placa resetar. O except (OSError, ValueError) cobre os dois problemas reais — arquivo que não existe e arquivo corrompido. E o config.update(salvo) sobre uma cópia do padrão faz o programa continuar funcionando quando uma versão nova acrescenta uma configuração que o arquivo antigo não tinha.

Registro de leituras em CSV

O segundo uso é o registro (log): uma linha por leitura, com hora e valores. O formato CSV — valores separados por vírgula — abre direto numa planilha. O exemplo usa o circuito do DHT22 da trilha de sensores e o modo 'a' (append), que acrescenta ao fim do arquivo sem apagar o que já estava lá.

from machine import Pin
import dht
import os
import time

sensor = dht.DHT22(Pin(4))
ARQUIVO = 'leituras.csv'
LIMITE_BYTES = 150                    # pequeno de propósito, para ver a rotação

def rotacionar():
    try:
        tamanho = os.stat(ARQUIVO)[6]
    except OSError:
        return                        # ainda não existe
    if tamanho < LIMITE_BYTES:
        return
    try:
        os.remove(ARQUIVO + '.1')     # o mais antigo sai
    except OSError:
        pass
    os.rename(ARQUIVO, ARQUIVO + '.1')
    print('-- rotacionei: o cheio virou', ARQUIVO + '.1')

def registrar(t, u):
    rotacionar()
    novo = ARQUIVO not in os.listdir()
    with open(ARQUIVO, 'a') as f:
        if novo:
            f.write('segundos,temperatura,umidade\n')
        f.write('%d,%.1f,%.1f\n' % (time.time(), t, u))

time.sleep(2)
for i in range(12):
    sensor.measure()
    registrar(sensor.temperature(), sensor.humidity())
    time.sleep(2)

for nome in sorted(os.listdir()):
    if nome.startswith('leituras'):
        print(nome, os.stat(nome)[6], 'bytes')
with open(ARQUIVO) as f:
    print(f.read())

A rotação resolve um problema que todo registro tem: crescer para sempre até encher a flash. Quando o arquivo passa do limite, ele é renomeado para leituras.csv.1, o .1 anterior é apagado e um arquivo novo começa. Assim, o registro guarda sempre as leituras mais recentes num espaço máximo conhecido. Num projeto real, o limite seria bem maior — algumas dezenas de kilobytes —, e pode haver mais gerações (.1, .2, .3).

Desgaste da flash: por que não gravar toda hora

A memória flash não aguenta gravações infinitas. As memórias flash SPI usadas nos módulos da ESP32 são especificadas pelos fabricantes para cerca de 100 mil ciclos de apagar e gravar por setor — é o que diz, por exemplo, a folha de dados da família W25Q da Winbond, junto com uma retenção de dados de mais de 20 anos. O chip exato muda de placa para placa; o número é a ordem de grandeza.

Cem mil parece muito até se fazer a conta. Um programa que regrava o mesmo pequeno arquivo a cada segundo chega a cem mil gravações em pouco mais de 27 horas. O nivelamento de desgaste do LittleFS espalha essas gravações por muitos blocos, o que multiplica a vida útil — mas a melhor proteção é no programa:

  • não grave o que não mudou: compare com o último valor gravado antes de escrever a configuração;
  • junte leituras na RAM e grave em lotes, como na imagem abaixo;
  • grave com a frequência que o projeto precisa: a temperatura de uma sala raramente precisa de mais de uma leitura por minuto.
Linha do tempo com dez leituras, uma por minuto. Na primeira linha, cada leitura é gravada na flash: dez gravações. Na segunda, as leituras ficam numa lista na RAM e são gravadas de cinco em cinco: duas gravações. Abaixo, a rotação de arquivos: dados.csv cheio vira dados.1.csv, e o dados.2.csv antigo é apagado.
Juntar leituras na RAM e gravar em lotes reduz o desgaste da flash e o consumo; a rotação mantém o tamanho dos registros sob controle.

A gravação em lotes tem um preço: se a energia acabar, perdem-se as leituras que ainda estavam na lista. É uma troca, e quem decide é o projeto. Um registro de temperatura aceita perder alguns minutos; um contador de peças numa máquina não.

Binário com struct: o mesmo dado, menos espaço

O CSV é cômodo, mas gasta espaço: cada dígito é um byte, e cada linha leva vírgulas e a quebra de linha. Para registros grandes, o módulo struct grava os números em binário, num tamanho fixo por registro. O exemplo compara os dois formatos e mostra outra vantagem do tamanho fixo: dá para ir direto ao registro número n com seek(), sem ler o arquivo todo.

import struct
import os

FORMATO = '<IhH'                 # segundos (4 bytes), temperatura x10 (2), umidade x10 (2)
TAM = struct.calcsize(FORMATO)

leituras = [(i * 60, 215 + i % 7, 600 + i % 11) for i in range(100)]

with open('dados.csv', 'w') as f:
    for s, t, u in leituras:
        f.write('%d,%.1f,%.1f\n' % (s, t / 10, u / 10))

with open('dados.bin', 'wb') as f:
    for s, t, u in leituras:
        f.write(struct.pack(FORMATO, s, t, u))

print('CSV:', os.stat('dados.csv')[6], 'bytes')
print('binário:', os.stat('dados.bin')[6], 'bytes (%d por registro)' % TAM)

def registro(n):
    with open('dados.bin', 'rb') as f:
        f.seek(n * TAM)
        s, t, u = struct.unpack(FORMATO, f.read(TAM))
    return s, t / 10, u / 10

print('registro 42:', registro(42))
blocos, livres = os.statvfs('/')[2], os.statvfs('/')[3]
print('flash: %d de %d blocos de 4 KB livres' % (livres, blocos))

O preço do binário é que ele não abre numa planilha: é preciso um programa para ler. Uma divisão comum é registrar em binário na placa e converter para CSV só na hora de exportar.

Muitos arquivos pequenos ocupam mais do que parece

O LittleFS guarda arquivos de até 128 bytes dentro dos próprios metadados, sem gastar bloco. Acima disso, cada arquivo ocupa blocos inteiros de 4096 bytes. Na prática, cem arquivos de 200 bytes gastam cem blocos — 400 KB — para guardar 20 KB de dados. Prefira poucos arquivos maiores, com várias leituras cada, a um arquivo por leitura.

FastMindAssistente de IA local

A IA que lembra da sua vida e mora no seu computador

  • Memória de longo prazo com a fonte de cada resposta
  • Funciona offline: seus dados ficam no seu PC
  • Lê PDFs, slides, fotos e gravações de reunião
Baixar grátis para Windows

Plano Essencial grátis para sempre

NVS: configurações pequenas, chave e valor

Além do sistema de arquivos, o firmware da ESP32 tem a NVS (non-volatile storage), uma área da flash organizada como um dicionário: chaves de texto guardando inteiros de 32 bits ou blocos de bytes. O MicroPython acessa a NVS pela classe esp32.NVS, separada em espaços de nomes (namespaces), para que partes diferentes do programa não pisem nas chaves umas das outras.

import esp32

nvs = esp32.NVS('estacao')          # o espaço de nomes é criado se não existir
nvs.set_i32('reinicios', 7)
nvs.set_blob('rede', 'laboratorio')
nvs.commit()                        # sem commit, tudo se perde no próximo reset

print(nvs.get_i32('reinicios'))
buf = bytearray(32)
n = nvs.get_blob('rede', buf)       # lê no buffer e devolve quantos bytes
print(bytes(buf[:n]).decode())

O detalhe que mais pega é o commit(): a documentação do MicroPython avisa que, sem ele, as mudanças feitas pelos set_ se perdem no próximo reset. Outra regra: get_i32() e get_blob() dão OSError quando a chave não existe ou tem outro tipo, então envolva a leitura num try. A NVS é boa para poucos valores pequenos, como contadores e credenciais; para registros e textos grandes, use arquivos. No simulador, o módulo esp32 ainda não existe, e por isso esse trecho é só para ler.

Memória RTC: atravessando o sono profundo

Quando a ESP32 entra em deep sleep para economizar bateria, a RAM comum é desligada, e ao acordar o programa começa do zero, como depois de um reset. Uma pequena memória ligada ao relógio interno, a memória RTC, continua alimentada. No MicroPython, ela é acessada por machine.RTC().memory(): com um argumento, grava; sem argumento, lê. A documentação diz que o conteúdo sobrevive ao soft reset e ao machine.deepsleep(), e que o tamanho máximo na ESP32 é 2048 bytes por padrão.

import machine
import json

rtc = machine.RTC()
try:
    estado = json.loads(rtc.memory() or b'{}')
except ValueError:
    estado = {}

estado['acordou'] = estado.get('acordou', 0) + 1
print('acordei', estado['acordou'], 'vezes')
rtc.memory(json.dumps(estado))

machine.deepsleep(60_000)          # dorme 1 minuto e recomeça o main.py

É o lugar ideal para o que muda a cada despertar — um contador, a última leitura, um pequeno lote de leituras a gravar depois —, porque gravar ali não gasta a flash. O limite é que ela apaga quando a energia acaba de vez: o que precisa sobreviver a trocar a bateria vai para a flash. No simulador, deepsleep ainda não é simulado; o artigo sobre baterias trata do sono profundo em detalhe.

Cartão microSD: gigabytes

Quando 2 MiB não bastam — fotos, áudio, meses de leituras por segundo —, um cartão microSD num módulo leitor resolve. O MicroPython da ESP32 tem a classe machine.SDCard, e a referência rápida da ESP32 indica os pinos do slot 2 no ESP32 original: SCK no GPIO18, CS no GPIO5, MISO no GPIO19 e MOSI no GPIO23.

import machine
import vfs
import os

sd = machine.SDCard(slot=2)
vfs.mount(sd, '/sd')
print(os.listdir('/sd'))

with open('/sd/leituras.csv', 'a') as f:
    f.write('1,23.4,61.0\n')

vfs.umount('/sd')                   # antes de tirar o cartão

Depois do vfs.mount(), o cartão aparece como uma pasta, e todo o resto — open(), os.listdir(), a rotação de arquivos — funciona igual. Três cuidados: o cartão costuma vir formatado em FAT, que não tolera bem quedas de energia no meio de uma escrita, então feche os arquivos logo depois de gravar; desmonte com vfs.umount() antes de tirar o cartão; e confira se o módulo leitor trabalha com 3,3 V nos sinais, porque os pinos da ESP32 não aguentam 5 V. No simulador, o vfs e o SDCard ainda não existem.

EEPROM e FRAM: chips externos por I2C

Entre a NVS e o cartão SD há duas opções pequenas, baratas e ligadas pelo mesmo barramento I2C dos displays — o assunto do artigo I2C na ESP32:

  • a EEPROM AT24C32, comum nos módulos de relógio DS3231, guarda 4 KB (4096 × 8 bits). A folha de dados indica 1 milhão de ciclos de gravação, retenção de 100 anos, páginas de 32 bytes e até 5 ms para concluir cada gravação;
  • a FRAM MB85RC256V guarda 32 KB e aguenta 10¹² gravações por byte — na prática, dá para regravar um contador a cada leitura sem pensar em desgaste. A folha de dados indica ainda que ela não precisa da espera de gravação da EEPROM e funciona com I2C de até 1 MHz.

As duas usam endereços de memória de 16 bits, e o MicroPython já trata disso com o argumento addrsize do I2C:

from machine import I2C, Pin

i2c = I2C(0, scl=Pin(22), sda=Pin(21))
print(i2c.scan())                    # a EEPROM costuma aparecer em 0x50 (80)

ENDERECO = 0x50
i2c.writeto_mem(ENDERECO, 0x0000, b'ola', addrsize=16)    # grava na posição 0
import time; time.sleep_ms(5)        # a EEPROM leva até 5 ms para gravar
print(i2c.readfrom_mem(ENDERECO, 0x0000, 3, addrsize=16))

Na EEPROM, uma gravação não pode atravessar o limite de uma página de 32 bytes: escrever 10 bytes a partir da posição 28 faz os últimos 6 voltarem ao começo da mesma página. Grave em pedaços alinhados às páginas. A FRAM não tem essa limitação de página nem a espera.

Escolhendo onde guardar

Um resumo para decidir:

O que guardarOnde
leituras do momento, cálculosRAM (variáveis)
estado entre despertares do deep sleepmemória RTC
poucas configurações pequenas, contadoresNVS ou um config.json
configuração mais rica, textosarquivo JSON no LittleFS
registro de leituras (dias, semanas)CSV ou binário no LittleFS, em lotes e com rotação
meses de registro, arquivos grandescartão microSD
contador gravado a cada leitura, sem desgasteFRAM
poucos dados num módulo de relógio que já tem EEPROMEEPROM AT24C32

E, em todos os casos, as mesmas regras de bom comportamento: feche os arquivos (o with faz isso), trate os erros de arquivo que não existe ou está estragado, não grave o que não mudou e confira o espaço livre com os.statvfs() antes de encher a memória.

Para continuar

O projeto final usa um arquivo de configuração na lição configuração da estação, e a lição de JSON mostra o formato em detalhe. A trilha de sensores e a lição DHT22 fornecem os dados para registrar, e a lição Timer, da trilha Tempo sem travar, mostra como ler e gravar no intervalo certo sem parar o resto do programa.

Os artigos sobre baterias e autonomia, que usa a memória RTC no sono profundo, e sobre o servidor web na placa, que pode salvar o que um formulário recebe, continuam este assunto. Para os pinos do cartão SD e do I2C, consulte o mapa de pinos e a ficha da ESP32 DevKit V1.

Fontes

Para praticar no PyPlaca

Trilhas

Lições

Fichas de componentes

FastMindAssistente de IA local

A IA que lembra da sua vida e mora no seu computador

  • Memória de longo prazo com a fonte de cada resposta
  • Funciona offline: seus dados ficam no seu PC
  • Lê PDFs, slides, fotos e gravações de reunião
Baixar grátis para Windows

Plano Essencial grátis para sempre

FastTeacherPara professores

Da ementa ao semestre planejado

  • Plano de ensino e calendário montados sozinhos
  • Diário de aulas pronto para colar no SIGA
  • Slides e listas de exercícios com gabarito
Conhecer o FastTeacher

Versão Community grátis · Windows

FastMindAssistente de IA local

A IA que lembra da sua vida e mora no seu computador

  • Memória de longo prazo com a fonte de cada resposta
  • Funciona offline: seus dados ficam no seu PC
  • Lê PDFs, slides, fotos e gravações de reunião
Baixar grátis para Windows

Plano Essencial grátis para sempre