MCU.TEC Padrões de Projetos Fixed-Sized Buffer Pattern

Fixed-Sized Buffer Pattern


📖 Origem: Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems – Bruce Powel Douglass

Resumo

O Fixed-Sized Buffer Pattern é um padrão de gerenciamento de memória que utiliza buffers de tamanho fixo e pré-alocados para armazenar dados temporários em sistemas embarcados. Ele é amplamente utilizado para comunicação entre módulos do sistema, filas de mensagens, buffers de entrada e saída e armazenamento intermediário de dados. A sua principal vantagem é a previsibilidade do uso da memória, evitando fragmentação e problemas de desempenho associados à alocação dinâmica. Esse padrão se mostra essencial para sistemas de tempo real, onde a confiabilidade da memória é um fator crítico.

Problema a ser resolvido

Muitos sistemas embarcados precisam armazenar temporariamente dados antes de processá-los. Isso pode ocorrer, por exemplo, em buffers de recepção de comunicação serial, aquisição de sinais analógicos ou manipulação de pacotes de rede. A alocação dinâmica de memória, como o uso de malloc() e free(), pode levar à fragmentação e falhas inesperadas, além de introduzir latências imprevisíveis que prejudicam o desempenho em aplicações de tempo real.

Outro problema comum ocorre quando buffers de tamanho variável são usados, pois isso dificulta a previsão do consumo de RAM. Em sistemas embarcados, onde cada byte de memória é valioso, a má gestão desses buffers pode levar a desperdício de memória ou esgotamento precoce dos recursos disponíveis. Além disso, sistemas que dependem de alocação dinâmica podem sofrer falhas catastróficas caso a memória se esgote durante a execução, causando travamentos ou reinicializações inesperadas.

O Fixed-Sized Buffer Pattern resolve esses problemas ao pré-alocar um buffer de tamanho fixo, garantindo um consumo de memória estável e previsível. Dessa forma, evita-se a fragmentação e garante-se um tempo de acesso constante aos dados, permitindo que sistemas críticos operem sem interrupções.

Estrutura do Padrão

A estrutura do padrão se baseia na alocação de um bloco de memória de tamanho fixo no momento da inicialização do sistema. Esse bloco pode ser organizado como um array circular ou uma lista estática de elementos. O gerenciamento do buffer é feito por índices ou ponteiros que controlam a posição de leitura e escrita, garantindo que os dados sejam armazenados e recuperados de forma ordenada.

Além da estrutura principal do buffer, há também um gerenciador de heap, que pode ser responsável por distribuir múltiplos buffers conforme a necessidade da aplicação. A lista de blocos livres mantém o controle sobre quais partes do buffer estão disponíveis para escrita, evitando sobrescrita acidental. Já o segmento de memória contém o espaço físico onde os dados são armazenados.

Outros elementos importantes incluem a fábrica de objetos, que pode criar e liberar instâncias de buffers conforme necessário, e o heap dimensionado, que garante que os buffers alocados tenham um tamanho predefinido e constante.

Papéis de Colaboração

  • Cliente: Solicita espaço no buffer para armazenar dados e recuperá-los posteriormente.
  • Produtor: Responsável por inserir dados no buffer, como sensores, módulos de comunicação ou periféricos.
  • Consumidor: Recupera os dados armazenados no buffer para processamento ou transmissão.
  • Lista de Blocos Livres: Mantém um registro das áreas disponíveis no buffer para novas alocações.
  • Gerenciador de Heap: Supervisiona a alocação e reutilização dos buffers de tamanho fixo.
  • Segmento de Memória: Armazena fisicamente os dados dentro do buffer pré-alocado.
  • Fábrica de Objetos: Pode ser usada para criar instâncias de buffers conforme a demanda do sistema.
  • Heap Dimensionado: Assegura que os buffers tenham tamanhos predefinidos e não sofram alterações durante a execução.

Consequências

O Fixed-Sized Buffer Pattern oferece diversas vantagens. Ele elimina a necessidade de alocação dinâmica de memória, reduzindo riscos de fragmentação e falhas inesperadas. Além disso, por definir um tamanho fixo para os buffers, o consumo de RAM é altamente previsível, permitindo que o sistema seja otimizado para uso eficiente dos recursos. Esse padrão também melhora a performance, pois evita as operações de alocação e desalocação que podem introduzir latências imprevisíveis.

Por outro lado, esse padrão pode levar a um certo desperdício de memória caso o tamanho do buffer seja superdimensionado. Se o buffer for maior do que necessário, a memória será ocupada sem ser utilizada de forma eficiente. Além disso, em sistemas com múltiplos buffers, pode ser necessário um mecanismo adicional para garantir a reutilização adequada das áreas de memória.

Estratégias de Implementação

Uma das formas mais comuns de implementar esse padrão é através de buffers circulares, onde os dados são armazenados em um array fixo e os índices de leitura e escrita são atualizados continuamente. Isso permite que o buffer seja reutilizado sem a necessidade de realocação de memória. Outra abordagem é utilizar listas de blocos fixos, onde cada bloco é tratado como uma unidade independente de armazenamento.

Para garantir acesso seguro ao buffer em sistemas multitarefa, pode ser necessário o uso de mutexes ou semáforos para evitar condições de corrida. Outra estratégia comum é implementar verificações de integridade no buffer, como a adição de checksums para detectar corrupção de dados. Em alguns casos, pode ser interessante combinar esse padrão com o Priority Ceiling Pattern, garantindo que tarefas críticas tenham acesso prioritário ao buffer.

Padrões Relacionados

O Fixed-Sized Buffer Pattern possui uma forte relação com outros padrões de gerenciamento de memória e comunicação:

  • Pool Allocation Pattern: Ambos utilizam blocos de memória predefinidos para evitar fragmentação.
  • Message Queuing Pattern: Pode utilizar buffers fixos para armazenar mensagens temporárias antes de processá-las.
  • Critical Section Pattern: Pode ser necessário para evitar condições de corrida ao acessar buffers compartilhados.
  • Garbage Compactor Pattern: Pode ser útil em sistemas que precisam reorganizar a memória quando múltiplos buffers são usados.

Exemplo de Código

Aqui está um exemplo otimizado da implementação do Fixed-Sized Buffer Pattern em C:

Abaixo está um diagrama em blocos que exemplifica o uso do padrão Fixed-Sized Buffer Pattern, veja código a seguir que representa de forma mais realista o padrão de projeto estudado.

#include <stdint.h>
// Tipos de dados padronizados
#include <stdbool.h> 
// Booleanos

#define SMALL_BLOCK_SIZE  64
#define MEDIUM_BLOCK_SIZE 128
#define LARGE_BLOCK_SIZE  256
#define BLOCKS_PER_POOL  10  // Número de blocos fixos por pilha
typedef enum { SMALL, MEDIUM, LARGE } BlockSize;

// Estrutura do bloco reutilizável
typedef struct Block {
    struct Block* next;
} Block;

// Estrutura da pilha de blocos

typedef struct {
    Block* free_list;
    uint8_t memory_pool[BLOCKS_PER_POOL][LARGE_BLOCK_SIZE]; // Buffer estático máximo
    uint8_t block_size;
} BlockStack;

// Estrutura do gerenciador de pilhas
typedef struct {
    BlockStack small_stack;
    BlockStack medium_stack;
    BlockStack large_stack;
} StackManager;
// Estrutura da fábrica de objetos
typedef struct {
    StackManager* stack_manager;
} ObjectFactory;

// Inicializa uma pilha de blocos (pré-alocação)
void init_stack(BlockStack* stack, uint8_t block_size) {
    stack->block_size = block_size;
    stack->free_list = NULL;
    // Divide o buffer estático em blocos reutilizáveis
    for (int i = 0; i < BLOCKS_PER_POOL; i++) {
        Block* block = (Block*)&stack->memory_pool[i][0];
        block->next = stack->free_list;
        stack->free_list = block;
    }
}

// Aloca um bloco reutilizando a lista livre
void* allocate_block(BlockStack* stack) {
    if (!stack->free_list) {
        return NULL; // Nenhum bloco disponível
    }
    Block* block = stack->free_list;
    stack->free_list = block->next;
    return block;<br>}

// Libera um bloco de volta para a pilha
void free_block(BlockStack* stack, void* block) {
    if (!block) return;
    ((Block*)block)->next = stack->free_list;
    stack->free_list = (Block*)block;
}

// Inicializa o gerenciador de pilhas
void init_stack_manager(StackManager* manager) {
    init_stack(&manager->small_stack, SMALL_BLOCK_SIZE);
    init_stack(&manager->medium_stack, MEDIUM_BLOCK_SIZE);
    init_stack(&manager->large_stack, LARGE_BLOCK_SIZE);
}

// Aloca um bloco baseado no tamanho especificado
void* allocate_from_manager(StackManager* manager, BlockSize size) {
    switch (size) {
      case SMALL: 
           return allocate_block(&manager->small_stack);
      case MEDIUM: 
           return allocate_block(&manager->medium_stack);
       case LARGE: 
           return allocate_block(&manager->large_stack)
    }
    return NULL;
}
// Libera um bloco baseado no tamanho especificado

void free_to_manager(StackManager* manager, void* block, BlockSize size) {
    switch (size) {
        case SMALL: free_block(&manager->small_stack, block); break;
        case MEDIUM: free_block(&manager->medium_stack, block); break;
        case LARGE: free_block(&manager->large_stack, block); break;
   }
}

// Inicializa a fábrica de objetos
void init_object_factory(ObjectFactory* factory, StackManager* manager) {
    factory->stack_manager = manager;<br>}<br><br>// Cria um objeto utilizando a fábrica<br>void* create_object(ObjectFactory* factory, BlockSize size) {<br>    return allocate_from_manager(factory->stack_manager, size);<br>}<br><br>// Libera um objeto utilizando a fábrica<br>void destroy_object(ObjectFactory* factory, void* object, BlockSize size) {
    free_to_manager(factory->stack_manager, object, size);
}

// Função de exemplo para testar no microcontrolador
void test_memory_allocation() {
    StackManager manager;
    ObjectFactory factory;
    init_stack_manager(&manager);
    init_object_factory(&factory, &manager);
    
    void* obj1 = create_object(&factory, SMALL);
    void* obj2 = create_object(&factory, MEDIUM);
    void* obj3 = create_object(&factory, LARGE);
    
    if (obj1 && obj2 && obj3) {
        // Simula LED aceso para indicar sucesso
        // gpio_set_level(LED_GPIO, 1);
    }
    
    destroy_object(&factory, obj1, SMALL);
    destroy_object(&factory, obj2, MEDIUM);
    destroy_object(&factory, obj3, LARGE);
    
    // Simula LED apagado para indicar memória liberada<br>   
    // gpio_set_level(LED_GPIO, 0);<br>
}

Material de apoio no GitHub

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