image

Access unlimited bootcamps and 650+ courses forever

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

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

    Share
    Recommended for you
    GFT - Fundamentos de Cloud com AWS
    Bootcamp Bradesco - GenAI, Dados & Cyber
    Bootcamp Afya - Automação de Dados com IA
    Comments (0)