O Linker é uma das etapas mais críticas no processo de construção de um firmware para sistemas embarcados. Sua função é consolidar diversos arquivos de código objeto em um único arquivo executável, organizando como os dados e instruções serão posicionados na memória do microcontrolador. Em arquiteturas como a do STM32 (baseada em ARM Cortex-M) e do ESP32 (baseada em Tensilica Xtensa), o controle de onde e como os segmentos de código e dados são alocados é fundamental para o correto funcionamento do sistema. Esse controle é feito por meio de scripts de Linker (.ld) e, no caso do ESP32, também com o arquivo partitions.csv
, que define o particionamento da memória Flash.
Objetivo da Seção
Apresentar uma visão geral do papel do Linker no processo de construção de firmware, destacando:
- O que é o Linker e qual sua função;
- Como ele é usado para posicionar variáveis e trechos de código em regiões específicas da memória;
- A diferença entre seu uso em plataformas STM32 e ESP32;
- A importância dos arquivos
.ld
epartitions.csv
no projeto embarcado.
Conceito Básico
Durante a compilação, o código-fonte (em C ou C++) é transformado em arquivos objeto .o
. O Linker pega esses arquivos e os combina, resolvendo endereços de funções, referências cruzadas entre variáveis globais e posicionando tudo de acordo com o layout de memória definido pelo script .ld
. Esse script age como um mapa da memória, especificando as regiões onde devem ser colocados:
- Código (
.text
); - Dados inicializados (
.data
); - Dados não inicializados (
.bss
); - Pilha e heap;
- Tabelas de vetores de interrupção e memória de inicialização.
Plataformas em Destaque
- STM32: Usa arquivos
.ld
customizados que refletem a memória RAM e Flash, com seções bem definidas para bootloaders, vetores de interrupção, código principal, etc. - ESP32: Usa também arquivos
.ld
, mas sua complexidade aumenta com a presença de múltiplas CPUs, caches e particionamento de memória Flash definido pelopartitions.csv
.
Estrutura de um Arquivo LD: seções e símbolos
O script do Linker, geralmente com extensão .ld
, descreve como o firmware será disposto na memória do dispositivo. Ele segue a sintaxe da linguagem de scripts do GNU Linker (ld
) e define regiões de memória física e a atribuição de seções do programa nessas regiões. Essa estrutura garante que o código e os dados sejam colocados em áreas apropriadas da RAM e Flash.
Definindo a Memória
No início do arquivo .ld
, define-se o layout físico da memória com a diretiva MEMORY
. Essa seção especifica os nomes, tamanhos e endereços base da RAM, Flash e outras regiões, como SRAM2 ou CCM no STM32, ou RTC_SLOW na RAM do ESP32.
Exemplo – STM32:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
Exemplo – ESP32:
MEMORY
{
iram0_0_seg (RX) : org = 0x40080000, len = 0x20000
dram0_0_seg (RW) : org = 0x3FFAE000, len = 0x20000
}
Seções Padrão
A diretiva SECTIONS
define como as seções do código objeto (geradas pelo compilador) são mapeadas nas regiões de memória definidas anteriormente.
Principais seções:
.isr_vector
: Vetor de interrupções (normalmente colocado no início da Flash)..text
: Código do programa..rodata
: Dados somente leitura (constantes)..data
: Dados inicializados na RAM..bss
: Variáveis globais não inicializadas (zeradas na inicialização)..heap
/.stack
: Reservas para alocação dinâmica e pilha de execução.
Trecho típico:
SECTIONS
{
.text :
{
KEEP(*(.isr_vector))
*(.text*)
*(.rodata*)
} > FLASH
.data : AT (ADDR(.text) + SIZEOF(.text))
{
*(.data*)
} > RAM
.bss :
{
*(.bss*)
*(COMMON)
} > RAM
}
Símbolos Especiais
Os scripts .ld
frequentemente definem símbolos que serão usados no código C para indicar, por exemplo, o início e fim da RAM, do stack, ou a posição de carga de dados:
_end = .;
_estack = ORIGIN(RAM) + LENGTH(RAM);
No código C, podem ser declarados como:
extern uint32_t _estack;
extern uint32_t _end;
Observações Importantes
- A ordem das seções é essencial: o que estiver listado primeiro será posicionado primeiro na memória.
- A diretiva
KEEP()
impede que o Linker remova seções não referenciadas, comum em vetores de interrupção. - A diretiva
AT()
é usada para indicar onde os dados residem na Flash antes de serem copiados para a RAM.
Diferenças entre os arquivos LD no STM32 e no ESP32
Embora tanto o STM32 quanto o ESP32 utilizem scripts .ld
para definir o mapeamento de memória, existem diferenças estruturais e conceituais importantes entre essas plataformas. Essas diferenças são motivadas pelas arquiteturas distintas de hardware, modelos de inicialização e tipos de memória disponíveis.
STM32 – Arquitetura Simples e Direta
O STM32, baseado na arquitetura ARM Cortex-M, segue uma abordagem linear e previsível no mapeamento de memória. A memória Flash normalmente começa em 0x08000000
, e a RAM em 0x20000000
. A posição do vetor de interrupções (reset vector) é crucial, pois a CPU começa a executar a partir desse ponto após o reset.
Características:
- O arquivo
.ld
do STM32 é quase sempre dividido em FLASH e RAM; - Contém o vetor de interrupções na primeira posição (
.isr_vector
); - Define símbolos como
_estack
para controle da pilha; - Permite personalizações simples para bootloaders ou seções específicas de RAM (como DTCM, SRAM2, CCMRAM, etc.);
- Ferramentas como STM32CubeIDE geram
.ld
automaticamente com base no modelo selecionado.
Exemplo típico de layout:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
ESP32 – Arquitetura Multinível e Particionada
O ESP32 é mais complexo: possui múltiplos tipos de RAM (DRAM, IRAM, RTC RAM), cache externa e bootloader configurável. A memória Flash é externa e compartilhada entre o bootloader, partições de aplicativo, sistema de arquivos (SPIFFS/FFat) e NVS (armazenamento não-volátil).
Características:
- Utiliza vários arquivos
.ld
para diferentes segmentos (esp32.ld
,memory.ld
,sections.ld
, etc.); - Define múltiplas regiões:
iram0
,dram0
,rtc_slow
,rtc_fast
, etc.; - Utiliza a diretiva
INCLUDE
para modularizar o layout; - Integra o conceito de partições, definidas no arquivo
partitions.csv
; - O bootloader da Espressif localiza a partição de aplicação na Flash e carrega o código para a IRAM ou executa diretamente da Flash (via cache).
Trecho exemplo de esp32.ld
:
MEMORY
{
iram0_0_seg : org = 0x40080000, len = 0x20000
dram0_0_seg : org = 0x3FFAE000, len = 0x20000
rtc_slow_seg : org = 0x50000000, len = 0x2000
}
Principais Diferenças Resumidas
Aspecto | STM32 | ESP32 |
---|---|---|
Arquitetura | Cortex-M (monocore ou dual core) | Tensilica Xtensa (dual core + co-processador) |
Memória Flash | Interna | Externa via SPI |
RAM | Linear e simples | Vários tipos: DRAM, IRAM, RTC RAM |
Arquivo .ld | Simples, único | Fragmentado, com includes |
Particionamento | Manual (caso de bootloaders) | Definido via partitions.csv |
Bootloader | Opcional ou fixo | Obrigatório e configurável |
Entendendo o partitions.csv no ESP32
Resumo
O arquivo partitions.csv
é um componente essencial nos projetos com ESP32. Ele define como a memória Flash externa será particionada para armazenar diferentes partes do firmware, como o bootloader, a aplicação principal, o sistema de arquivos (SPIFFS, FATFS, LittleFS), armazenamento de chaves criptográficas, dados de calibragem de RF, entre outros. É o que garante que o firmware saiba onde cada bloco de dados deve ser gravado e lido na memória externa.
O que é o partitions.csv
?
É um arquivo de texto, em formato CSV (Comma-Separated Values), que define as partições da Flash para uso pelo bootloader e sistema operacional (no caso do ESP-IDF). Ele é convertido em um binário de tabela de partições que o bootloader carrega durante a inicialização.
Formato do Arquivo
Cada linha do partitions.csv
define uma partição com os seguintes campos:
nome, tipo, subtipo, endereço, tamanho, flags
Exemplo comum:
# Nome Tipo Subtipo Endereço Tamanho Flags
nvs data nvs 0x9000 0x5000
otadata data ota 0xe000 0x2000
app0 app ota_0 0x10000 1M
app1 app ota_1 0x110000 1M
spiffs data spiffs 0x210000 0xF0000
Campos Explicados
- nome: Identificador da partição (pode ser usado no código C, por exemplo para montar SPIFFS).
- tipo:
app
: usado para partições de firmware.data
: para armazenamento persistente (ex: NVS, SPIFFS).
- subtipo:
factory
,ota_0
,ota_1
: partições de aplicação.nvs
,spiffs
,fat
,ota
, etc.: tipos específicos de dados.
- endereço: onde na memória Flash essa partição começa (em hexadecimal).
- tamanho: em bytes ou usando sufixos (
K
,M
). - flags: parâmetros opcionais (ex:
encrypted
para criptografia).
Como o partitions.csv é usado
- Durante a compilação, o arquivo é processado e gera um binário chamado
partition_table.bin
. - Durante a gravação (flash), esse binário é gravado em um endereço fixo (tipicamente
0x8000
). - Durante o boot, o bootloader do ESP32 lê essa tabela para saber onde está cada partição.
- No código, você pode montar sistemas de arquivos ou acessar partições diretamente com base nos nomes definidos no CSV.
Customização
Você pode modificar o partitions.csv
para incluir, por exemplo:
- Mais partições OTA para atualizações em lote;
- Uma partição para armazenamento FAT montado via
esp_vfs_fat_mount
; - Uma partição para arquivos estáticos com LittleFS;
- Tamanhos reduzidos para aplicações mínimas.
Exemplo com FAT:
storage data fat 0x300000 512K
Estratégias de Construção de Arquivos LD: práticas e ferramentas
Resumo
A criação e manutenção de arquivos .ld
pode ser feita manualmente ou gerenciada por ferramentas automáticas. Em sistemas embarcados, a escolha da estratégia depende da complexidade da aplicação, do ambiente de desenvolvimento e da necessidade de controle sobre o uso da memória. Nesta seção, exploramos como construir e adaptar arquivos de Linker para STM32 e ESP32, além das ferramentas que auxiliam no processo.
Construção Manual de Arquivos .ld
A escrita manual é útil em projetos onde se deseja controle total sobre a organização de memória — especialmente para bootloaders, realocação de código crítico em RAM, ou para usar áreas especiais como DTCM, CCM ou RTC.
Dicas práticas:
- Sempre comece definindo corretamente as regiões com
MEMORY
. - Use
SECTIONS
para mapear precisamente o conteúdo dos binários. - Defina símbolos auxiliares (
_end
,_stack_start
) para controle no código C. - Use
KEEP()
para evitar que o Linker remova trechos importantes como vetores de interrupção. - Comente abundantemente o arquivo
.ld
— erros silenciosos podem causar falhas difíceis de depurar.
Uso de Ferramentas:
Para STM32:
- STM32CubeIDE / STM32CubeMX:
- Gera automaticamente arquivos
.ld
com base no modelo do microcontrolador selecionado. - Permite configuração de áreas específicas de memória (ex: RAM2, CCMRAM).
- Reorganiza o linker script ao incluir bibliotecas HAL ou FreeRTOS.
- Gera automaticamente arquivos
- GNU Arm Toolchain (
arm-none-eabi-ld
):- O script
.ld
é passado na linha de comando usando-T
. - Pode ser usado em Makefiles ou CMake para projetos personalizados.
- O script
Para ESP32:
- ESP-IDF:
- Utiliza um conjunto modular de scripts
.ld
organizados em:memory.ld
: define as regiões de memória disponíveis.sections.ld
: organiza onde o código e dados são colocados.esp32.ld
: script de entrada que inclui os anteriores.
- Permite criar fragmentos de linker (
ld
fragments) no diretório do componente para colocar variáveis em regiões específicas, como IRAM.
- Utiliza um conjunto modular de scripts
Exemplo de fragmento:
myvar (noinit) : ALIGN(4)
{
_myvar_start = .;
KEEP(*(.myvar_section))
_myvar_end = .;
} > DRAM
- idf.py linkmap:
- Gera o mapa de ligação (
.map
) detalhado, útil para depuração de problemas de alocação de memória.
- Gera o mapa de ligação (
Boas Práticas
- Sempre gere e revise o mapa de ligação (
.map
) após compilar. Ele mostra o endereço final de cada seção. - Valide o tamanho das regiões para evitar sobreposição.
- Em projetos com OTA, certifique-se de que as partições app0/app1 não ultrapassem o tamanho máximo.
- Use
ASSERT()
no script.ld
para verificar restrições em tempo de link:
ASSERT(SIZEOF(.data) < 0x2000, "Data section too big!")
Casos Práticos: Customizando o Linker para necessidades reais
Resumo
Customizar o script de Linker permite criar soluções otimizadas para aplicações embarcadas que demandam desempenho, economia de energia ou acesso rápido à memória. A seguir, são apresentados exemplos reais de customizações em projetos com STM32 e ESP32, incluindo alocação de variáveis em regiões específicas, execução de código diretamente na RAM, buffers persistentes e uso de seções noinit.
Executar código diretamente na RAM (STM32)
Para funções críticas em tempo real — como interrupções com baixa latência — pode ser necessário colocá-las na RAM, onde a execução é mais rápida que na Flash.
Etapas:
- Criar uma seção
.ramfunc
no script:
.ramfunc :
{
*(.ramfunc*)
} > RAM
- Declarar a função no código com atributo:
__attribute__((section(".ramfunc"))) void critical_ISR(void) {
// Código de alta prioridade
}
- (Opcional) Garantir cópia da Flash para RAM durante a inicialização usando
AT()
e símbolos auxiliares para copiar os dados.
2Criar buffers fixos em local específico da RAM (ESP32 e STM32)
Se você precisa garantir que um buffer de DMA fique em uma região não cacheada da memória (ou alinhada), é possível forçar sua posição.
ESP32 – usando fragmento de linker:
- Crie um arquivo
linker_fragment.ld
no componente:
__dma_buffer_start = .;
.dma_buffer (NOLOAD):
{
. = ALIGN(32);
KEEP(*(.dma_buffer))
. = ALIGN(32);
} > DRAM
__dma_buffer_end = .;
- No código:
__attribute__((section(".dma_buffer"))) uint8_t dma_rx[256];
3. Buffer persistente entre resets (seção .noinit
)
A seção .noinit
impede que variáveis sejam zeradas na inicialização. Isso é útil para armazenar dados temporários entre resets, watchdogs ou soft reboots.
Script LD:
.noinit (NOLOAD):
{
*(.noinit*)
} > RAM
Código C:
__attribute__((section(".noinit"))) uint32_t retained_counter;
⚠️ Importante: você é responsável por inicializar essa variável, pois o C startup não o fará.
4. Espaço reservado para bootloader ou firmware OTA
Em projetos com bootloaders, você pode reservar o início da Flash para ele e mover o início da aplicação:
MEMORY
{
BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 16K
APP_FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 496K
}
E ajustar o início da seção .text
para a nova região APP_FLASH
.
5. Dividindo a RAM em blocos com finalidades distintas
No STM32 com RAMs separadas (SRAM1, SRAM2), você pode mover buffers grandes para a RAM auxiliar:
MEMORY
{
RAM1 (xrw): ORIGIN = 0x20000000, LENGTH = 96K
RAM2 (xrw): ORIGIN = 0x20018000, LENGTH = 32K
}
.bigbuffers (NOLOAD):
{
*(.bigbuffer*)
} > RAM2
Esses exemplos ilustram o poder e a flexibilidade dos scripts de Linker quando bem utilizados. Customizá-los pode ser a chave para alcançar desempenho, estabilidade e segurança em sistemas embarcados críticos.
Considerações Finais e Dicas de Diagnóstico
A compreensão e personalização de scripts de Linker são habilidades essenciais para engenheiros de sistemas embarcados. Erros nesse estágio podem resultar em falhas silenciosas, corrupção de dados, mau uso de memória e comportamento imprevisível do firmware. Esta seção final destaca boas práticas, armadilhas comuns e ferramentas úteis para diagnóstico e validação do layout de memória.
Boas Práticas ao Trabalhar com .ld
- Comente extensivamente cada bloco do script para documentar a intenção de uso de cada região.
- Use símbolos nomeados (_start, _end, stack_top) para facilitar o acesso no código C.
- Divida responsabilidades em arquivos menores com
INCLUDE
(usado amplamente no ESP-IDF). - Crie seções específicas para dados críticos, buffers, código de interrupção ou dados de boot.
- Teste variações de layout com segurança em projetos de bootloader + aplicação OTA.
Validação com o Arquivo .map
Após a compilação, o GCC gera um arquivo de mapa de ligação (.map
), contendo o endereço real de cada símbolo, função e variável alocada.
Dicas para leitura do .map
:
- Verifique se há sobreposição entre seções;
- Confirme se buffers grandes foram realmente colocados em RAM auxiliar;
- Localize variáveis em
.bss
que estão ocupando muito espaço; - Confirme se a tabela de interrupções foi posicionada corretamente (ex: início da Flash).
Uso de ASSERTs no Linker
Para evitar erros silenciosos, você pode usar ASSERT()
no script .ld
para impor limites:
ASSERT(SIZEOF(.data) <= 0x2000, "Erro: a seção .data ultrapassou o limite da RAM!")
Isso impede que a compilação gere um firmware que vá ultrapassar o espaço físico disponível.
Ferramentas de Apoio
- objdump (
arm-none-eabi-objdump -h
): mostra as seções e onde foram colocadas. - nm (
arm-none-eabi-nm
): lista símbolos com endereços — útil para variáveis específicas. - size (
arm-none-eabi-size
): resume o tamanho das seções.text
,.data
e.bss
. - idf.py size-components (ESP32): mostra quanto cada componente ocupa na Flash/RAM.
- STM32CubeIDE: gera visualizações gráficas do uso de memória, além de adaptar o linker automaticamente.
Armadilhas Comuns
- Esquecer de alinhar buffers de DMA (pode causar falha silenciosa).
- Colocar código na IRAM sem definir corretamente as seções no ESP32.
- Deixar variáveis grandes na
.data
(copiadas da Flash para a RAM, consumindo tempo e espaço). - Não usar
NOLOAD
em seções como.noinit
, resultando em perda de dados após o reset.
Conclusão
Scripts de Linker não são apenas “detalhes técnicos”, mas componentes essenciais que controlam como o firmware funciona na prática. Dominar sua estrutura, lógica e sintaxe permite que você construa sistemas mais rápidos, estáveis e confiáveis — explorando ao máximo o hardware disponível, seja no robusto STM32 ou no versátil ESP32.