image

Acesse bootcamps ilimitados e +650 cursos pra sempre

70
%OFF
Article image
Carlos Pinheiro
Carlos Pinheiro18/05/2026 18:06
Compartilhe

Como funciona a distribuição de memória em aplicações e microcontroladores

    Quando comecei a estudar programação mais próxima do hardware, percebi que entender memória era uma das chaves para escrever programas melhores. Em linguagens de alto nível, muitas vezes usamos variáveis, objetos, funções e bibliotecas sem pensar onde tudo isso fica armazenado. Mas, quando entramos no mundo dos microcontroladores, essa abstração desaparece rapidamente: cada byte importa.

    Neste tutorial, quero apresentar de forma simples como a memória costuma ser organizada em um computador e como essa organização aparece, de forma mais limitada e direta, em um microcontrolador.

    1. A memória em um computador

    Em um computador comum, quando executamos um programa, o sistema operacional cria um espaço de memória para aquela aplicação. Esse espaço não representa necessariamente a memória física diretamente, mas sim uma visão virtual da memória. O sistema operacional, junto com a unidade de gerenciamento de memória, conhecida como MMU, organiza esse espaço para que cada programa “pense” que possui sua própria região de memória.

    De forma simplificada, podemos imaginar a memória de um processo assim:

    Memória virtual de um processo em um computador
    
    Endereços altos
    +-----------------------------+
    | Stack                       |
    | Pilha de execução           |
    | Cresce para baixo           |
    +-----------------------------+
    |                             |
    | Espaço livre                |
    |                             |
    +-----------------------------+
    | Heap                        |
    | Memória dinâmica            |
    | Cresce para cima            |
    +-----------------------------+
    | BSS                         |
    | Variáveis globais zeradas   |
    +-----------------------------+
    | Data                        |
    | Variáveis globais iniciadas |
    +-----------------------------+
    | Text / Code                 |
    | Código do programa          |
    +-----------------------------+
    Endereços baixos
    

    A região Text, ou Code, contém as instruções do programa. É ali que fica o código compilado. Em muitos sistemas, essa região é marcada como somente leitura, para evitar que o programa modifique suas próprias instruções acidentalmente.

    A região Data armazena variáveis globais e estáticas que possuem valor inicial definido. Por exemplo:

    int contador = 10;
    

    Já a região BSS guarda variáveis globais e estáticas que começam zeradas:

    int sensor_estado;
    static int erro_count;
    

    Essas variáveis não precisam ocupar espaço completo no arquivo executável, porque o carregador do sistema operacional sabe que deve inicializá-las com zero ao iniciar o programa.

    A região Heap é usada para alocação dinâmica de memória. Em C, por exemplo, usamos malloc, calloc, realloc e free. Em linguagens como JavaScript, Python ou Java, muitos objetos são criados internamente no heap, embora a linguagem esconda isso do programador.

    A Stack, ou pilha, é usada para chamadas de função, variáveis locais e controle de retorno. Cada vez que uma função é chamada, um novo bloco de dados é empilhado. Quando a função termina, esse bloco é removido.

    2. Exemplo simples em C

    Veja este exemplo:

    #include <stdio.h>
    #include <stdlib.h>
    
    int global_inicializada = 100;  // Data
    int global_zerada;             // BSS
    
    void exemplo(void)
    {
      int local = 10;             // Stack
    
      int *dinamico = malloc(sizeof(int)); // Heap
      *dinamico = 50;
    
      printf("%d\n", local);
    
      free(dinamico);
    }
    
    int main(void)
    {
      exemplo();
      return 0;
    }
    

    A distribuição conceitual seria:

    +-----------------------------+
    | Stack                       |
    | local                       |
    | dinamico                    |
    +-----------------------------+
    | Heap                        |
    | valor alocado por malloc    |
    +-----------------------------+
    | BSS                         |
    | global_zerada               |
    +-----------------------------+
    | Data                        |
    | global_inicializada         |
    +-----------------------------+
    | Text                        |
    | main(), exemplo(), printf() |
    +-----------------------------+
    

    Um detalhe importante: a variável dinamico em si fica na stack, mas o espaço apontado por ela fica no heap. Ou seja, o ponteiro é uma variável local, mas o conteúdo alocado dinamicamente está em outra região.

    3. A memória em um microcontrolador

    Em um microcontrolador, a situação muda bastante. Normalmente não temos um sistema operacional completo, não temos memória virtual e muitas vezes não temos MMU. O programa trabalha diretamente com regiões físicas de memória.

    Em um microcontrolador típico, temos pelo menos duas memórias principais:

    Microcontrolador típico
    
    +-----------------------------+
    | Flash                       |
    | Código do programa          |
    | Constantes                  |
    | Vetor de interrupções       |
    +-----------------------------+
    
    +-----------------------------+
    | SRAM                        |
    | Variáveis em execução       |
    | Stack                       |
    | Heap, se existir            |
    | Buffers                     |
    +-----------------------------+
    

    A Flash é uma memória não volátil. Isso significa que ela mantém o conteúdo mesmo sem energia. É nela que o firmware fica gravado.

    A SRAM é memória volátil. Ela perde o conteúdo quando o microcontrolador é desligado. É usada durante a execução do programa para variáveis, pilha, buffers de comunicação, dados de sensores e estruturas temporárias.

    Uma distribuição simplificada de memória em um microcontrolador pode ser vista assim:

    FLASH
    
    Endereços baixos
    +-----------------------------+
    | Vetor de interrupções       |
    +-----------------------------+
    | Código do programa          |
    | Funções                     |
    +-----------------------------+
    | Constantes                  |
    | Tabelas somente leitura     |
    +-----------------------------+
    Endereços altos
    SRAM
    
    Endereços baixos
    +-----------------------------+
    | Data                        |
    | Variáveis globais iniciadas |
    +-----------------------------+
    | BSS                         |
    | Variáveis globais zeradas   |
    +-----------------------------+
    | Heap                        |
    | Cresce para cima            |
    +-----------------------------+
    | Espaço livre                |
    +-----------------------------+
    | Stack                       |
    | Cresce para baixo           |
    +-----------------------------+
    Endereços altos
    

    A diferença principal é que, no computador, o sistema operacional ajuda a organizar e proteger a memória. No microcontrolador, essa responsabilidade fica muito mais próxima do programador e do script de linker.

    4. O papel do linker script

    No desenvolvimento embarcado, o linker script define onde cada parte do programa será colocada. Ele informa ao compilador e ao linker quais regiões existem, onde começa a Flash, qual o tamanho da SRAM, onde fica o código, onde ficam as variáveis e onde começa a pilha.

    Um exemplo conceitual seria:

    MEMORY
    {
      FLASH : origem = 0x08000000, tamanho = 512K
      RAM   : origem = 0x20000000, tamanho = 128K
    }
    

    Isso significa que o programa será gravado na Flash a partir do endereço 0x08000000, enquanto as variáveis em tempo de execução usarão a RAM a partir de 0x20000000.

    Em microcontroladores ARM Cortex-M, por exemplo, é comum o vetor de interrupções ficar no início da Flash. Esse vetor contém o endereço inicial da stack e os endereços das funções de tratamento de interrupção.

    FLASH em um Cortex-M
    
    0x08000000
    +-----------------------------+
    | Endereço inicial da Stack   |
    +-----------------------------+
    | Reset_Handler               |
    +-----------------------------+
    | NMI_Handler                 |
    +-----------------------------+
    | HardFault_Handler           |
    +-----------------------------+
    | Outros vetores              |
    +-----------------------------+
    | Código do firmware          |
    +-----------------------------+
    

    5. Stack e Heap no microcontrolador

    A stack é essencial em qualquer aplicação em C. Ela guarda variáveis locais, endereços de retorno e contexto de chamadas de função. Em sistemas com interrupções, a stack também pode ser usada para salvar registradores temporariamente.

    Por exemplo:

    void leitura_sensor(void)
    {
      int valor_adc = 0;
      float tensao = 0.0f;
    }
    

    As variáveis valor_adc e tensao normalmente ficam na stack.

    O heap, por outro lado, nem sempre é recomendado em microcontroladores pequenos. Usar malloc e free pode causar fragmentação de memória, principalmente em sistemas que ficam ligados por muito tempo.

    Problema possível com heap
    
    +-----------------------------+
    | Bloco usado                 |
    +-----------------------------+
    | Espaço livre pequeno        |
    +-----------------------------+
    | Bloco usado                 |
    +-----------------------------+
    | Espaço livre pequeno        |
    +-----------------------------+
    | Bloco usado                 |
    +-----------------------------+
    

    Mesmo que a soma dos espaços livres seja grande, talvez nenhum bloco livre seja grande o bastante para uma nova alocação. Esse é o problema da fragmentação.

    Por isso, em firmware embarcado, muitas vezes preferimos buffers estáticos:

    #define BUFFER_SIZE 256
    
    uint8_t buffer_uart[BUFFER_SIZE];
    uint16_t amostras_adc[128];
    

    Esses buffers ficam em regiões globais, normalmente em BSS ou Data, dependendo se foram inicializados ou não.

    6. Comparação direta: computador versus microcontrolador

    Computador
    
    +-----------------------------+
    | Sistema operacional         |
    | Memória virtual             |
    | Proteção entre processos    |
    | Heap abundante              |
    | Stack por thread            |
    | Arquivos e bibliotecas      |
    +-----------------------------+
    Microcontrolador
    
    +-----------------------------+
    | Sem sistema operacional     |
    | Memória física direta       |
    | Pouca RAM                   |
    | Flash limitada              |
    | Stack pequena               |
    | Heap opcional               |
    | Controle via linker script  |
    +-----------------------------+
    

    No computador, se um programa precisa de mais memória, o sistema operacional pode gerenciar páginas, memória virtual e até swap em disco. No microcontrolador, se a RAM acabou, acabou. O firmware pode travar, corromper dados ou entrar em comportamento indefinido.

    7. Um exemplo visual completo

    Imagine um pequeno firmware com leitura de ADC, comunicação UART e controle de LED:

    #include <stdint.h>
    
    const char firmware_nome[] = "Sensor ADC"; // Flash / rodata
    
    uint16_t adc_buffer[128];                  // BSS
    uint32_t contador = 1;                     // Data
    
    void adc_read(void)
    {
      uint16_t valor_local = 0;              // Stack
      valor_local = 1234;
    }
    
    int main(void)
    {
      while (1)
      {
          adc_read();
      }
    }
    

    A memória poderia ser imaginada assim:

    FLASH
    +-----------------------------+
    | Vetor de interrupções       |
    +-----------------------------+
    | main()                      |
    | adc_read()                  |
    +-----------------------------+
    | "Sensor ADC"               |
    +-----------------------------+
    SRAM
    +-----------------------------+
    | Data                        |
    | contador = 1                |
    +-----------------------------+
    | BSS                         |
    | adc_buffer[128]             |
    +-----------------------------+
    | Heap                        |
    | talvez não usado            |
    +-----------------------------+
    | Espaço livre                |
    +-----------------------------+
    | Stack                       |
    | valor_local                 |
    | retorno de adc_read()       |
    +-----------------------------+
    

    8. Conclusão

    Quando entendo a distribuição de memória, passo a programar com mais consciência. Em um computador, posso contar com o sistema operacional, memória virtual, bibliotecas robustas e mecanismos de proteção. Em um microcontrolador, preciso pensar de maneira mais direta: onde está meu código, onde estão minhas variáveis, quanto de RAM tenho, quanto minha stack pode crescer e se realmente vale a pena usar heap.

    Essa visão é fundamental para quem programa sistemas embarcados. Muitos erros difíceis de encontrar, como travamentos aleatórios, estouro de pilha, corrupção de variáveis e falhas em interrupções, nascem justamente de uma má compreensão da memória. Por isso, antes de otimizar código, usar RTOS ou adicionar bibliotecas, é importante olhar para o mapa de memória e entender como o firmware realmente ocupa o hardware.

    Conheça mais sobre Microcontroladores em https://mcu.tec.br

    Compartilhe
    Recomendados para você
    GFT - Fundamentos de Cloud com AWS
    Bootcamp Bradesco - GenAI, Dados & Cyber
    Bootcamp Afya - Automação de Dados com IA
    Comentários (1)
    Ronaldo Schmidt
    Ronaldo Schmidt - 18/05/2026 19:32

    Olá.

    Excelente artigo.

    Grato por compartilhar.

    Esse é um tema extremamente importante no desenvolvimento embarcado e, ao mesmo tempo, um dos mais difíceis de absorver no início.

    Quando começamos a entender como a memória é realmente organizada passamos a programar com muito mais consciência e previsibilidade.

    Muitos desenvolvedores acabam seguindo o caminho “mais fácil”, focando apenas em fazer o código funcionar e deixando os problemas de memória para serem corrigidos depois.

    O problema é que, em sistemas embarcados, esses erros normalmente aparecem como falhas difíceis de diagnosticar: travamentos aleatórios, corrupção de dados, comportamento instável em interrupções e estouro de pilha.

    Por isso, entender o mapa de memória e como o firmware ocupa o hardware deveria ser uma das primeiras preocupações de quem trabalha com microcontroladores, antes mesmo de pensar em otimização, RTOS ou bibliotecas mais complexas.

    Mas ao mesmo tempo existe um ponto muito real no desenvolvimento: mesmo tomando todos esses cuidados, muitos problemas ainda acabam surgindo.

    Às vezes se gastam horas em planejamento, prevenção e organização, e na prática o comportamento real do sistema só aparece durante os testes em hardware.

    Por isso acredito muito em uma abordagem equilibrada ,mas sem deixar o desenvolvimento travar pelo excesso de preocupação com problemas que talvez nunca aconteçam.

    Em muitos casos, o cliente exige entregas contínuas, evolução rápida e resultados constantes. Fica difícil seguir todas as regras de engenharia “a ferro e fogo” sem comprometer prazo e produtividade.

    No fim, grande parte da experiência vem justamente do ciclo contínuo de implementar, testar bastante no hardware real, identificar falhas, corrigir e evoluir o firmware conforme a necessidade do projeto.

    O conhecimento teórico reduz muitos erros, mas a prática e os testes constantes continuam sendo indispensáveis.

    Comenta ai sua opinião.