Exemplo conceitual usando LittleFS em uma Flash externa no STM32N6

Até agora, trabalhamos com SD Card, onde a escolha natural é usar FATFS. Agora vamos mudar o cenário: imagine que o projeto com STM32N6 também possui uma memória Flash externa SPI, QSPI ou OctoSPI. Nesse caso, o microcontrolador passa a controlar diretamente operações de leitura, escrita e apagamento da Flash. É aqui que o LittleFS se torna uma opção muito interessante.
O LittleFS não acessa o hardware diretamente. Ele precisa de uma camada de adaptação, normalmente chamada de block device layer, que fornece quatro operações principais: ler, programar, apagar e sincronizar. Isso é uma boa decisão de projeto, porque separa o sistema de arquivos da memória física. O LittleFS cuida da organização dos arquivos, enquanto o desenvolvedor implementa como a Flash externa será acessada.
O ponto central é que o LittleFS trabalha com blocos apagáveis. Diferente de uma RAM, a Flash não permite simplesmente alterar qualquer byte livremente. Em geral, antes de escrever novamente em uma região, é necessário apagar um setor inteiro. O LittleFS foi projetado justamente para lidar com esse tipo de limitação, distribuindo escritas e protegendo metadados contra corrupção em muitos cenários de reset inesperado.
A configuração básica do LittleFS começa com uma estrutura lfs_config. Nela informamos os tamanhos de leitura, programação, bloco, cache, quantidade de blocos e ponteiros para as funções de acesso ao hardware.
#include "lfs.h"
#include <string.h>
#include <stdio.h>
/*
* Instância global do LittleFS.
*/
static lfs_t lfs;
/*
* Arquivo manipulado pelo LittleFS.
*/
static lfs_file_t file;
/*
* Protótipos da camada de blocos.
* Essas funções devem ser adaptadas ao driver real da Flash externa.
*/
static int stm32_flash_read(
const struct lfs_config *config,
lfs_block_t block,
lfs_off_t offset,
void *buffer,
lfs_size_t size
);
static int stm32_flash_prog(
const struct lfs_config *config,
lfs_block_t block,
lfs_off_t offset,
const void *buffer,
lfs_size_t size
);
static int stm32_flash_erase(
const struct lfs_config *config,
lfs_block_t block
);
static int stm32_flash_sync(
const struct lfs_config *config
);
/*
* Configuração do LittleFS.
* Os valores abaixo são exemplos e devem ser ajustados ao datasheet
* da memória Flash usada no projeto.
*/
static const struct lfs_config littlefs_config = {
.read = stm32_flash_read,
.prog = stm32_flash_prog,
.erase = stm32_flash_erase,
.sync = stm32_flash_sync,
.read_size = 16,
.prog_size = 16,
.block_size = 4096,
.block_count = 1024,
.cache_size = 256,
.lookahead_size = 128,
.block_cycles = 500,
};
Os valores acima são apenas uma referência didática. O block_size, por exemplo, deve normalmente acompanhar o tamanho do setor apagável da Flash externa. Se a memória apaga setores de 4 KiB, faz sentido usar block_size = 4096. O block_count representa quantos blocos desse tamanho serão usados pelo sistema de arquivos. Se usarmos 1024 blocos de 4096 bytes, teremos aproximadamente 4 MiB reservados ao LittleFS.
Agora precisamos implementar as funções de acesso à Flash. Como este artigo é genérico, vamos deixar as funções reais de driver representadas por funções abstratas, como ExternalFlash_Read, ExternalFlash_Program e ExternalFlash_EraseSector.
#define EXTERNAL_FLASH_BASE_ADDR 0x00000000UL
/*
* Função auxiliar para converter bloco + deslocamento
* em endereço físico dentro da Flash externa.
*/
static uint32_t littlefs_get_address(
const struct lfs_config *config,
lfs_block_t block,
lfs_off_t offset
)
{
return EXTERNAL_FLASH_BASE_ADDR +
((uint32_t)block * config->block_size) +
(uint32_t)offset;
}
static int stm32_flash_read(
const struct lfs_config *config,
lfs_block_t block,
lfs_off_t offset,
void *buffer,
lfs_size_t size
)
{
uint32_t address = littlefs_get_address(config, block, offset);
if (ExternalFlash_Read(address, buffer, size) != 0)
{
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
static int stm32_flash_prog(
const struct lfs_config *config,
lfs_block_t block,
lfs_off_t offset,
const void *buffer,
lfs_size_t size
)
{
uint32_t address = littlefs_get_address(config, block, offset);
if (ExternalFlash_Program(address, buffer, size) != 0)
{
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
static int stm32_flash_erase(
const struct lfs_config *config,
lfs_block_t block
)
{
uint32_t address = littlefs_get_address(config, block, 0);
if (ExternalFlash_EraseSector(address) != 0)
{
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
static int stm32_flash_sync(
const struct lfs_config *config)
{
(void)config;
/*
* Em algumas memórias, pode ser necessário aguardar
* o fim de operações internas de escrita/apagamento.
*/
if (ExternalFlash_WaitReady() != 0)
{
return LFS_ERR_IO;
}
return LFS_ERR_OK;
}
Esse código mostra o padrão essencial de integração. O LittleFS informa qual bloco e qual deslocamento deseja acessar. A camada de adaptação converte isso em um endereço físico da memória Flash e chama o driver real. Em um STM32N6, esse driver pode usar SPI, QSPI ou OctoSPI, dependendo da placa e da memória externa usada.
Com a configuração pronta, podemos montar o sistema de arquivos:
static int LittleFS_Init(void)
{
int result;
result = lfs_mount(&lfs, &littlefs_config);
if (result != LFS_ERR_OK)
{
/*
* Se a montagem falhar, podemos formatar.
* Em produto final, essa decisão deve ser tomada com cuidado,
* pois formatar apaga os dados existentes.
*/
result = lfs_format(&lfs, &littlefs_config);
if (result != LFS_ERR_OK)
{
return result;
}
result = lfs_mount(&lfs, &littlefs_config);
if (result != LFS_ERR_OK)
{
return result;
}
}
return LFS_ERR_OK;
}
Em um produto real, não é recomendável formatar automaticamente sem algum critério adicional. Uma falha temporária de hardware, alimentação instável ou problema de inicialização poderia fazer o firmware apagar dados importantes. O ideal é ter uma política de recuperação: tentar montar, verificar assinatura, validar versão, talvez tentar uma segunda vez, registrar erro e só formatar se houver comando explícito ou se a aplicação permitir.
Agora podemos criar um arquivo de configuração simples:
static int LittleFS_WriteConfig(void)
{
const char *json =
"{\n"
" \"device\": \"STM32N6\",\n"
" \"storage\": \"LittleFS\",\n"
" \"sample_rate_hz\": 1000\n"
"}\n";
int result = lfs_file_open(
&lfs,
&file,
"/config.json",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC
);
if (result != LFS_ERR_OK)
{
return result;
}
lfs_ssize_t written = lfs_file_write(
&lfs,
&file,
json,
strlen(json)
);
if (written < 0)
{
lfs_file_close(&lfs, &file);
return (int)written;
}
result = lfs_file_sync(&lfs, &file);
if (result != LFS_ERR_OK)
{
lfs_file_close(&lfs, &file);
return result;
}
result = lfs_file_close(&lfs, &file);
return result;
}
A leitura do arquivo segue a mesma ideia:
static int LittleFS_ReadConfig(void)
{
char buffer[256];
int result = lfs_file_open(
&lfs,
&file,
"/config.json",
LFS_O_RDONLY
);
if (result != LFS_ERR_OK)
{
return result;
}
lfs_ssize_t bytesRead = lfs_file_read(
&lfs,
&file,
buffer,
sizeof(buffer) - 1
);
if (bytesRead < 0)
{
lfs_file_close(&lfs, &file);
return (int)bytesRead;
}
buffer[bytesRead] = '\0';
printf("Configuração lida do LittleFS:\r\n%s\r\n", buffer);
return lfs_file_close(&lfs, &file);
}
Com FreeRTOS, o uso pode ficar dentro de uma tarefa dedicada, de forma semelhante ao que fizemos com o SD Card:
#include "cmsis_os.h"
void LittleFSTask(void *argument)
{
if (LittleFS_Init() == LFS_ERR_OK)
{
LittleFS_WriteConfig();
LittleFS_ReadConfig();
}
for (;;)
{
osDelay(1000);
}
}
Esse exemplo mostra que a lógica de arquivo é parecida com FATFS: montar, abrir, escrever, sincronizar, fechar e ler. A grande diferença está na camada inferior. No FATFS com SD Card, o sistema trabalha sobre setores de um dispositivo de bloco. No LittleFS, o sistema trabalha sobre blocos apagáveis de Flash, e por isso precisa de funções explícitas de read, prog, erase e sync.
O LittleFS é uma escolha muito boa para guardar configurações, pequenos bancos de parâmetros, arquivos JSON, certificados, logs moderados e dados que precisam sobreviver a resets inesperados. Ele não substitui o SD Card quando o objetivo é armazenar grandes volumes removíveis e compatíveis com PC, mas complementa muito bem um projeto embarcado moderno.
Em uma arquitetura robusta para STM32N6, poderíamos usar os dois:
SD Card + FATFS
Dados grandes, logs exportáveis, arquivos CSV, binários e datasets.
Flash externa + LittleFS
Configurações, estado interno, chaves públicas, metadados e parâmetros.
Essa divisão é muito prática. O SD Card fica responsável por dados de volume e intercâmbio com o computador. A Flash externa com LittleFS fica responsável por dados internos do produto, onde robustez e controle são mais importantes do que compatibilidade direta com o PC.