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



