MCU.TEC geral Introdução ao Linker em Sistemas Embarcados

Introdução ao Linker em Sistemas Embarcados


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 e partitions.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 pelo partitions.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

AspectoSTM32ESP32
ArquiteturaCortex-M (monocore ou dual core)Tensilica Xtensa (dual core + co-processador)
Memória FlashInternaExterna via SPI
RAMLinear e simplesVários tipos: DRAM, IRAM, RTC RAM
Arquivo .ldSimples, únicoFragmentado, com includes
ParticionamentoManual (caso de bootloaders)Definido via partitions.csv
BootloaderOpcional ou fixoObrigató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

  1. Durante a compilação, o arquivo é processado e gera um binário chamado partition_table.bin.
  2. Durante a gravação (flash), esse binário é gravado em um endereço fixo (tipicamente 0x8000).
  3. Durante o boot, o bootloader do ESP32 lê essa tabela para saber onde está cada partição.
  4. 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.
  • 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.

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.

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.

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:

  1. Criar uma seção .ramfunc no script:
.ramfunc :
{
  *(.ramfunc*)
} > RAM
  1. Declarar a função no código com atributo:
__attribute__((section(".ramfunc"))) void critical_ISR(void) {
  // Código de alta prioridade
}
  1. (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:

  1. 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 = .;
  1. 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.


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