MCU & FPGA geral Sistemas de Arquivos em Microcontroladores: LittleFS, SPIFFS e SD Card no STM32N6

Sistemas de Arquivos em Microcontroladores: LittleFS, SPIFFS e SD Card no STM32N6

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.

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x