O que é um Bootloader?
O Bootloader é o primeiro segmento de código executado em um sistema embarcado após a inicialização. Depois de concluir a inicialização da CPU e do hardware relevante, ele carrega a imagem do sistema operacional ou o programa aplicativo embarcado na memória e, em seguida, faz a transição para o espaço onde o sistema operacional reside, iniciando assim a operação do sistema operacional. Esse processo é fundamental para qualquer pessoa envolvida na programação de microcontroladores específicos, como o STM32.
Semelhante a um programa aplicativo, um Bootloader é um programa independente que contém componentes essenciais, como código de inicialização, interrupções, um programa principal (função Boot_main) e, opcionalmente, um sistema operacional. Apesar de seu tamanho reduzido, o Bootloader abrange funcionalidades críticas.
Os bootloaders são normalmente muito dependentes do hardware e são especialmente significativos no domínio dos sistemas embarcados. Como resultado, é quase impossível criar um bootloader universalmente aplicável no domínio embarcado. No entanto, ainda podemos generalizar alguns conceitos sobre bootloaders para orientar os usuários no projeto e na implementação de bootloaders específicos.
Modos de operação de um bootloader
A maioria dos bootloaders compreende dois modos de operação distintos: o modo de inicialização e o modo de download.
Ao ligar o sistema, o bootloader inicializa o ambiente de software e hardware do sistema e seleciona um dos modos de operação com base nas condições atuais do hardware. Isso envolve configurar o modo de operação da CPU, inicializar a memória, desativar interrupções e lidar com tarefas como desligar a MMU/Cache.
Modo de carregamento da inicialização:
Este modo, também conhecido como modo "autónomo", envolve o Boot Loader carregar autonomamente o sistema operativo a partir de um dispositivo de armazenamento de estado sólido na máquina de destino para a RAM. Todo este processo ocorre sem qualquer intervenção do utilizador. Este modo representa o funcionamento normal do Boot Loader.
Modo de download:
Neste modo, o Boot Loader na máquina de destino inicia a comunicação através de meios como conexões seriais ou de rede com uma máquina host para baixar arquivos. Os arquivos obtidos da máquina host são normalmente armazenados na RAM da máquina de destino pelo Boot Loader antes de serem gravados na memória Flash ou em um dispositivo de armazenamento de estado sólido semelhante na máquina de destino.
Como funciona um bootloader?
Existem dois tipos de processo de inicialização para um Bootloader: Single-Stage e Multi-Stage. Geralmente, os Boot Loaders multiestágio possuem funcionalidades mais complexas e portabilidade aprimorada. Os Boot Loaders que são iniciados a partir de dispositivos de armazenamento de estado sólido geralmente usam um processo de dois estágios, dividido em estágio 1 e estágio 2: o estágio 1 realiza a inicialização do hardware, prepara o espaço de memória para o estágio 2, copia o estágio 2 para a memória, configura a pilha e, em seguida, faz a transição para o estágio 2.
Bootloader Fase 1
Inicialização do dispositivo de hardware
- Desativar todas as interrupções: O tratamento das interrupções é normalmente da responsabilidade dos drivers de dispositivo do sistema operacional, portanto, o Boot Loader pode ignorar as respostas às interrupções durante sua execução. O mascaramento de interrupções pode ser obtido modificando o registro de máscara de interrupção ou o registro de status da CPU (como o registro CPSR da ARM).
- Definir a velocidade da CPU e a frequência do relógio.
- Inicializar a RAM: Isso inclui configurar corretamente os registros de função do controlador de memória do sistema e vários registros de controle do banco de memória.
- Inicialização do LED: os LEDs são frequentemente acionados por meio do GPIO para indicar o status do sistema (OK ou Erro). Se não houver LEDs, a inicialização do UART para imprimir o logotipo do Boot Loader ou informações de caracteres por meio da comunicação serial pode servir para esse fim.
- Desativar o cache interno de instruções/dados da CPU.
Prepare espaço na RAM para carregar o Bootloader Stage 2
Para uma execução mais rápida, o estágio 2 é normalmente carregado na RAM. Portanto, um intervalo de memória disponível deve ser alocado para carregar o estágio 2 do Boot Loader.
Como o estágio 2 geralmente contém código em linguagem C, o espaço necessário deve considerar tanto o tamanho executável do estágio 2 quanto o espaço da pilha. Além disso, o espaço deve, preferencialmente, estar alinhado com o tamanho da página de memória (normalmente 4 KB). Geralmente, 1 MB de espaço na RAM é suficiente. O intervalo de endereços específico pode ser escolhido arbitrariamente. Por exemplo, uma abordagem comum é alocar a imagem executável do estágio 2 para execução em um espaço de 1 MB a partir do endereço base da RAM do sistema, 0xc0200000. No entanto, alocar o estágio 2 para o 1 MB superior de todo o espaço da RAM (ou seja, (RamEnd-1MB) – RamEnd) é uma estratégia recomendada.
Vamos denotar o tamanho do intervalo de espaço de RAM alocado como "stage2_size" (em bytes) e os endereços inicial e final como "stage2_start" e "stage2_end" (ambos os endereços alinhados a limites de 4 bytes). Assim:
stage2_end = stage2_start + stage2_size
Além disso, é imperativo garantir que o intervalo de endereços alocado seja realmente um espaço de RAM gravável e legível. Para garantir isso, é necessário testar o intervalo de endereços alocado. Um método de teste adequado, como o usado pelo "blob", envolve testar as duas primeiras palavras de cada página de memória quanto à capacidade de leitura e gravação.
Copiar o Stage 2 do Boot Loader para o espaço RAM
Para fazer isso, certifique-se de dois pontos:
- A imagem executável da localização do estágio 2 no dispositivo de armazenamento de estado sólido.
- O endereço inicial do espaço RAM.
Definir ponteiro de pilha (SP)
A configuração do ponteiro de pilha (sp) prepara a execução do código em linguagem C. Normalmente, o valor do sp pode ser definido como (stage2_end-4), representando o limite superior do espaço de 1 MB de RAM alocado na seção 3.1.2 (a pilha cresce para baixo).
Além disso, antes de definir o ponteiro da pilha, é possível desativar o LED como um sinal para os usuários de que uma transição para o estágio 2 é iminente.
Seguindo essas etapas de execução, o layout da memória física do sistema deve se assemelhar ao diagrama abaixo.

Ir para o ponto de entrada C do estágio 2
Depois que tudo acima estiver pronto, você pode pular para o estágio 2 do Boot Loader para executar. Por exemplo, em sistemas ARM, isso pode ser feito modificando o registro do PC para o endereço apropriado. O layout da memória do sistema quando a imagem executável do estágio 2 do bootloader acaba de ser copiada para o espaço RAM é mostrado na figura acima.
Bootloader Fase 2
Inicialização do dispositivo de hardware
- Inicialize pelo menos uma porta serial para comunicação de saída de E/S com usuários de terminais.
- Inicialize temporizadores e outros componentes de hardware.
Antes de inicializar esses dispositivos, também é possível acender o LED para indicar o início da execução da função main(). Após a inicialização do dispositivo, determinadas informações, como strings de nomes de programas e números de versão, podem ser exibidas.
Detecção do mapeamento da memória do sistema
O mapeamento de memória refere-se à alocação de intervalos de endereços dentro de todo o espaço de endereçamento físico de 4 GB para endereçar unidades de RAM do sistema. Por exemplo, na CPU SA-1100, um espaço de endereçamento de 512 MB a partir de 0xC000,0000 serve como espaço de endereçamento RAM do sistema. No caso da CPU Samsung S3C44B0X, um espaço de endereço de 64 MB entre 0x0c00,0000 e 0x1000,0000 é usado para o espaço de endereço RAM do sistema. Embora as CPUs normalmente reservem uma parte substancial do espaço de endereço para a RAM do sistema, nem todo o espaço de endereço RAM reservado pode ser utilizado na construção de sistemas embarcados específicos. Assim, os sistemas incorporados frequentemente mapeiam apenas uma parte do espaço de endereço RAM reservado da CPU para unidades RAM, deixando parte do espaço de endereço RAM reservado sem uso. Diante desse fato, o estágio 2 do Boot Loader deve examinar todo o mapeamento de memória do sistema antes de tentar qualquer ação (como ler uma imagem do kernel armazenada na flash para o espaço RAM). Ele precisa saber quais partes do espaço de endereço RAM reservado da CPU estão genuinamente mapeadas para unidades de endereço RAM e quais estão em um estado "não utilizado".
Descrição do mapeamento de memória
A seguinte estrutura de dados pode ser usada para descrever um intervalo de endereços contínuo no espaço de endereços da RAM:
typedef struct memory_area_struct {
u32 start; /* the base address of the memory region */
u32 size; /* the byte number of the memory region */
int used;
} memory_area_t;
Esses intervalos de endereços contíguos dentro do espaço de endereçamento da RAM podem estar em um dos dois estados:
- used=1 indica que o intervalo de endereços contínuo foi implementado e está genuinamente mapeado para unidades de RAM.
- used=0 indica que o intervalo de endereços contínuo não está implementado no sistema e permanece sem uso.
Com base na estrutura de dados memory_area_t descrita acima, todo o espaço de endereço RAM reservado da CPU pode ser representado por uma matriz do tipo memory_area_t, conforme mostrado abaixo:
memory_area_t memory_map[NUM_MEM_AREAS] = {
[0 ... (NUM_MEM_AREAS - 1)] = {
.start = 0,
.size = 0,
.used = 0
},
};
Detecção de mapeamento de memória
Aqui está um algoritmo simples, mas eficaz, para detectar a situação do mapeamento de memória em todo o espaço de endereçamento da RAM:
/* Initialize the array */
for(i = 0; i < NUM_MEM_AREAS; i++)
memory_map[i].used = 0;
/* Write 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
*(u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
/*
* Check whether the address space starting from base address
* MEM_START + i * PAGE_SIZE, with a size of PAGE_SIZE, is a valid RAM address space.
*/
Call the algorithm test_mempage() from section 3.1.2;
if (current memory page is not a valid RAM page) {
/* no RAM here */
if (memory_map[i].used )
i++;
continue;
}
/*
* The current page is a valid address range mapped to RAM.
* However, we need to determine if it's an alias of some address page within the 4GB address space.
*/
if (*(u32 *)addr != 0) { /* alias? */
/* This memory page is an alias of an address page within the 4GB address space. */
if (memory_map[i].used )
i++;
continue;
}
/*
* The current page is a valid address range mapped to RAM,
* and it's not an alias of an address page within the 4GB address space.
*/
if (memory_map[i].used == 0) {
memory_map[i].start = addr;
memory_map[i].size = PAGE_SIZE;
memory_map[i].used = 1;
} else {
memory_map[i].size += PAGE_SIZE;
}
} /* end of for (…) */
Ao executar o algoritmo acima para detectar o status do mapeamento de memória do sistema, o Boot Loader também pode imprimir informações detalhadas sobre o mapeamento de memória na porta serial.
Carregando imagens do kernel e do sistema de arquivos raiz
Planejamento do layout da memória
Isso envolve dois aspectos:
- o intervalo de memória ocupado pela imagem do kernel;
- o intervalo de memória ocupado pelo sistema de arquivos raiz. Ao planejar o layout da memória, considere o endereço base e o tamanho das imagens.
Para a imagem do kernel, é comum copiá-la para um intervalo de memória a partir de (MEM_START + 0x8000), com aproximadamente 1 MB de tamanho (os kernels Linux incorporados geralmente têm menos de 1 MB). Por que deixar um espaço de 32 KB entre MEM_START e MEM_START + 0x8000? Isso ocorre porque o kernel Linux coloca certas estruturas de dados globais nesse segmento de memória, como parâmetros de inicialização e tabelas de páginas do kernel.
Para a imagem do sistema de arquivos raiz, ela geralmente é copiada para o local a partir de MEM_START + 0x0010,0000. Se estiver usando um Ramdisk como imagem do sistema de arquivos raiz, o tamanho descompactado é normalmente em torno de 1 MB.
Copiando do Flash
Como as CPUs incorporadas, como ARM, geralmente acessam Flash e outros dispositivos de armazenamento de estado sólido dentro de um espaço de endereço de memória unificado, a leitura de dados do Flash é semelhante à leitura de unidades RAM. Um loop simples é suficiente para copiar a imagem do dispositivo Flash:
while (count) {
*dest++ = *src++; /* they are all aligned with word boundary */
count -= 4; /* byte number */
};
Definindo os parâmetros de inicialização do kernel
Depois de copiar a imagem do kernel e a imagem do sistema de arquivos raiz para o espaço RAM, a inicialização do kernel Linux pode ser preparada. Mas antes de invocar o kernel, é necessária uma etapa de preparação: definir os parâmetros de inicialização do kernel Linux.
Os kernels Linux a partir da versão 2.4.x esperam que os parâmetros de inicialização sejam passados na forma de uma lista marcada. A lista marcada de parâmetros de inicialização começa com a marca ATAG_CORE e termina com a marca ATAG_NONE. Cada marca consiste em uma estrutura tag_header que identifica o parâmetro, seguida por estruturas de dados que contêm os valores dos parâmetros. As estruturas de dados tag e tag_header são definidas no arquivo de cabeçalho include/asm/setup.h do código-fonte do kernel Linux.
Em sistemas Linux incorporados, os parâmetros de inicialização comuns que precisam ser definidos pelo Boot Loader incluem ATAG_CORE, ATAG_MEM, ATAG_CMDLINE, ATAG_RAMDISK e ATAG_INITRD.
Por exemplo, veja como definir ATAG_CORE:
params = (struct tag *)BOOT_PARAMS;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);
Aqui, BOOT_PARAMS representa o endereço base inicial dos parâmetros de inicialização do kernel na memória, e o ponteiro params é do tipo struct tag. A macro tag_next() calcula o endereço inicial da próxima tag imediatamente após a tag atual. É importante observar que o ID do dispositivo para o sistema de arquivos raiz do kernel é definido aqui.
Abaixo está um exemplo de código para definir informações de mapeamento de memória:
for (i = 0; i < NUM_MEM_AREAS; i++) {
if (memory_map[i].used) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size(tag_mem32);
params->u.mem.start = memory_map[i].start;
params->u.mem.size = memory_map[i].size;
params = tag_next(params);
}
}
Na matriz memory_map[], cada segmento de memória válido corresponde a uma tag de parâmetro ATAG_MEM.
O kernel Linux pode receber informações como parâmetros de linha de comando durante a inicialização. Isso nos permite fornecer informações de parâmetros de hardware que o kernel não consegue detectar por conta própria ou substituir informações que o kernel detectou. Por exemplo, usamos a string de parâmetro de linha de comando "console=ttyS0,115200n8" para instruir o kernel a usar ttyS0 como console com as configurações "115200bps, sem paridade, 8 bits de dados". Aqui está um exemplo de código para definir a string do parâmetro da linha de comando do kernel:
char *p;
for (p = commandline; *p == ' '; p++)
;
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
strcpy(params->u.cmdline.cmdline, p);
params = tag_next(params);
Observe que, no código acima, ao definir o tamanho de tag_header, ele deve incluir o caractere de terminação '\0' na string e ser arredondado para o múltiplo mais próximo de 4 bytes, pois o membro size da estrutura tag_header representa o número de palavras.
Abaixo está um exemplo de código para definir ATAG_INITRD, indicando a localização na RAM onde a imagem initrd (em formato compactado) pode ser encontrada, juntamente com seu tamanho:
params->hdr.tag = ATAG_INITRD2;
params->hdr.size = tag_size(tag_initrd);
params->u.initrd.start = RAMDISK_RAM_BASE;
params->u.initrd.size = INITRD_LEN;
params = tag_next(params);
Por fim, defina a tag ATAG_NONE para concluir toda a lista de parâmetros de inicialização:
static void setup_end_tag(void) {
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
Invocando o Kernel
O Boot Loader chama o kernel Linux saltando diretamente para a primeira instrução do kernel, ou seja, saltando diretamente para o endereço MEM_START + 0x8000. As seguintes condições precisam ser atendidas ao saltar:
- Configurações do registro da CPU:
R0 = 0
R1 = ID do tipo de máquina (para o número do tipo de máquina, consulte linux/arch/arm/tools/mach-types)
R2 = Endereço base inicial da lista marcada de parâmetros de inicialização na RAM - Modo da CPU:
Desativar interrupções (IRQs e FIQs)
A CPU deve estar no modo SVC - Configurações de cache e MMU:
MMU deve estar desativada Cache
de instruções pode estar ativada ou desativada Cache
de dados deve estar desativada
Se estiver usando C, a invocação do kernel pode ser feita assim:
void (*theKernel)(int zero, int arch, u32 params_addr) =
(void (*)(int, int, u32))KERNEL_RAM_BASE;
theKernel(0, ARCH_NUMBER, (u32)kernel_params_start);
Observe que a chamada da função to theKernel() nunca deve retornar. Se retornar, ocorreu um erro.
Bootloader de sistema incorporado vs Bootloader de PC
Nas arquiteturas de PC, o bootloader consiste no BIOS (essencialmente firmware) e no bootloader do sistema operacional localizado no MBR do disco rígido (por exemplo, LILO, GRUB). Depois que o BIOS conclui a detecção de hardware e a alocação de recursos, ele carrega o bootloader do MBR do disco rígido na RAM do sistema e passa o controle para o bootloader do sistema operacional. A principal tarefa do bootloader é ler a imagem do kernel do disco rígido na RAM e, em seguida, saltar para o ponto de entrada do kernel para iniciar o sistema operacional.
Em sistemas embarcados, normalmente não há um programa de firmware como o BIOS (embora algumas CPUs embarcadas possam incluir um pequeno programa de inicialização embarcado). Consequentemente, toda a tarefa de carregamento e inicialização do sistema é realizada pelo bootloader. Por exemplo, em um sistema embarcado baseado no núcleo ARM7TDMI, o sistema normalmente inicia a execução no endereço 0x00000000 durante a inicialização ou reinicialização, e esse endereço geralmente contém o programa bootloader do sistema.
CPU e placa incorporada suportadas pelo Boot Loader
Cada arquitetura de CPU diferente tem um Boot Loader diferente. Alguns Boot Loaders também suportam CPUs com múltiplas arquiteturas. Por exemplo, o U-Boot suporta tanto a arquitetura ARM quanto a arquitetura MIPS. Além de depender da arquitetura da CPU, o Boot Loader também depende da configuração de dispositivos específicos incorporados ao nível da placa. Ou seja, o Boot Loader não é necessariamente adequado para as duas placas incorporadas diferentes, mesmo que elas sejam construídas com base na mesma CPU.
Meio de instalação do bootloader
Quando um sistema é ligado ou reiniciado, as CPUs geralmente buscam instruções de um endereço predeterminado definido pelo fabricante da CPU. Por exemplo, as CPUs baseadas no núcleo ARM7TDMI geralmente buscam sua primeira instrução do endereço 0x00000000 após uma reinicialização. Os sistemas embarcados construídos com base em arquiteturas de CPU geralmente mapeiam algum tipo de dispositivo de armazenamento de estado sólido (como ROM, EPROM ou FLASH) para esse endereço predeterminado. Portanto, após a inicialização do sistema, a CPU executa primeiro o programa bootloader.




