Uma explicação detalhada do processo de mapeamento e carga de inicialização do STM32.
Embora todos os exemplos abaixo sejam para STM32F446, os princípios básicos se aplicam à maioria dos MCUs.
O STM32CUBEMX produz STM32F446RETX_FLASH.LD, que veremos como referência para todas as nossas explorações.
O vinculador pega os arquivos de objeto produzidos pelo compilador e gera a saída de compilação final, que no nosso caso é o .ELF Binário. O vinculador sempre usa um arquivo de script de ligação; Mesmo se você não especificar um, um script padrão será usado.
Importante observar que o script do vinculador descreve apenas a memória com base nas especificações do MCU e não altera nenhum endereço de memória de hardware.
Não podemos falar sobre o processo de bootloader sem entender a estrutura da memória. Na verdade, é o objetivo do carregador de inicialização ter a memória em um estado pronto para executar o método main() do nosso aplicativo.
A memória do programa, a memória de dados, os registros e as portas de E/S no STM32F4 estão organizadas no mesmo espaço de endereço linear.
...
+ -- -- 0x2001FFFF -- -- +
| |
| RAM |
| |
+ -- -- 0x20000000 -- -- +
| ... |
+ -- -- 0x1FFF7A0F -- -- +
| |
| System |
| |
+ -- -- 0x1FFF0000 -- -- +
| ... |
+ -- -- 0x081FFFFF -- -- +
| |
| Flash |
| |
+ -- -- 0x08000000 -- -- +
| ... |
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + Onde a memória de alias está apontando para piscar, sistema ou memória RAM, dependendo do pino BOOT0 . Por padrão, é flash.
Dois espaços são mais interessantes para nós por enquanto:
No entanto, na prática, a divisão funcional poderia variar. Por exemplo, pode ser necessário carregar dados binários do programa na RAM ou vice -versa.
A estrutura da memória é refletida no arquivo do vinculador. O flash começa na ORIGIN = 0x8000000 e RAM na ORIGIN = 0x20000000 .
// linker file
MEMORY
{
RAM ( xrw ) : ORIGIN = 0x20000000 , LENGTH = 128 K
FLASH ( rx ) : ORIGIN = 0x8000000 , LENGTH = 512 K
}Observe que isso é apenas uma implementação padrão, você pode dividir facilmente o Flash e a RAM e adicionar seus próprios blocos ou seções assim que eles aderirem à especificação da memória do MCU.
A memória é então dividida no arquivo do vinculador em seções, cada uma com seu endereço e destino (por exemplo, flash ou RAM).
// linker file
SECTIONS
{
...
. text :
{
...
} > FLASH
...
}Para explorar a tabela de símbolos .Elf , usaremos o comando Arm-None-eabi-objdump e classificaremos todas as entradas por seus endereços:
arm-none-eabi-objdump -t stm32-boot-explained.elf | sortO programa produz saída conforme descrito em detalhes no manual:
Address Flag_Bits Section Size NameO código de programação compilado entra nesta seção na memória flash .
A seção começa no endereço ORIGIN do Flash e _etext aponta para o último endereço da seção.
08000000 l d . text 00000000 . text
08000 c00 g . text 00000000 _etextPodemos ver a localização das funções, que usamos em nosso arquivo de bootloader:
080005 c8 g F . text 00000048 __libc_init_array
08000610 g F . text 000000d 8 main
080006e8 g F . text 00000064 Reset_Handler
08000b c4 g F . text 00000024 SystemInit A seção reside na memória RAM e mantém todas as variáveis com valores definidos. O intervalo de endereços se estende de _sdata a _edata .
20000000 g . data 00000000 _sdata
20000010 g . data 00000000 _edataVamos verificar quais variáveis de main.c residem nesta seção:
20000000 l O . data 00000004 static_data_int
20000008 g O . data 00000008 data_double Os valores reais devem ser preenchidos por um script de bootloader, copiando dados da memória flash no endereço _sidata :
08000 c08 g * ABS * 00000000 _sidata Bloco Símbolo de partida Seção de dados de RAM para todas as variáveis sem valor atribuído. O carregador de inicialização cuida da definição de dados deste bloco para 0 .
O intervalo de memória abrange de _sbss a _ebss .
20000010 g . bss 00000000 _sbss
20000044 g . bss 00000000 _ebssTodas as variáveis de main.c sem valor explícito residem nesta seção:
2000002 c l O . bss 00000004 static_bss_int
20000030 g O . bss 00000008 bss_double
20000038 g O . bss 00000008 bss_my_struct
20000040 g O . bss 00000004 bss_my_unionVamos verificar com o GDB :
(gdb) p bss_double
$1 = 0
(gdb) p & bss_double
$2 = (double * ) 0x20000030 < bss_double > Depois de atribuir algum valor à variável, seu endereço não muda e ainda permanece em .bss :
(gdb) p bss_double
$3 = 86
(gdb) p & bss_double
$4 = (double * ) 0x20000030 < bss_double > Toda a memória da RAM acima _end e até _estack se dedicar à memória e empilhar a memória.
20000048 g . _user_heap_stack 00000000 _end
20020000 g . isr_vector 00000000 _estack _estack O endereço é calculado como ORIGIN(RAM) + LENGTH(RAM) . De modo que para 128kb RAM:
_estack = 0x20000000 + 128 * 1024 # dec
= 0x20000000 + 0x20000 # hex
= 0x20020000 A pilha é uma estrutura de Lifo que começa no _estack e cresce para baixo. O tamanho mínimo da pilha é definido no arquivo do vinculador como _Min_Stack_Size . A memória da pilha é automaticamente liberada.
+ -- -- 0x20020000 -- -- + < -- _estack
| |
| Stack |
| |
+ - - 0x2001FC00 - - + < -- - _Min_Stack_Size
| |
+ -- -- 0x200 sssss -- -- + < -- $msp register
| |
| |
| Free space |
| |
| |
+ -- -- 0x200 hhhhh -- -- + < -- Actual heap end
| |
| Heap |
| |
+ - - 0x20000248 - - + < -- + _Min_Heap_Size
| |
+ -- -- 0x20000048 -- -- + < -- _end Por sua vez, a pilha começa de _end e cresce para cima até _estack - _Min_Stack_Size quando solicitado por malloc .
Temos uma variável stack_int definida em Main.C. Vamos verificar o endereço com o GDB depois que ele foi inicializado:
(gdb) p & stack_int
$1 = (unsigned short * ) 0x2001ffd6
(gdb) p $msp
$2 = (void * ) 0x2001ffd0 Que corresponde às nossas expectativas, quando a variável está acima do $msp .
Vale a pena mencionar que a implementação da biblioteca C padrão comumente usada em aplicativos C incorporados é Newlib. Esta biblioteca requer a implementação de certas funções específicas do sistema. O STM32CUBEMX gera o arquivo syscalls.c com as implementações padrão necessárias.
É também o caso da chamada sbrk que aumenta o espaço de dados do programa. malloc está usando esta função para alocar mais memória de heap. Você pode encontrar uma implementação gerada pelo STM32CUBEMX no sysmem.c. Ele simplesmente permite que a pilha cresça de _end até _estack - _Min_Stack_Size .
Agora, quando entendemos a memória do MCUS, vamos nos conectar à nossa programação com o GDB , eis o que vemos como a primeira saída:
...
Reading symbols from ./stm32-boot-explained.elf...
Remote debugging using localhost:61234
Reset_Handler () at %PATH%/bootloader.c:25
25 void Reset_Handler () {
(gdb) info registers
...
pc 0x80006e8 0x80006e8 < Reset_Handler >
... Reset_Handler foi de alguma forma identificado como um ponto de inicialização para o nosso aplicativo.
Você deve ter notado que há uma instrução ENTRY no arquivo do vinculador:
// linker file
ENTRY ( Reset_Handler ) Na verdade, ele salva uma referência ao endereço de função Reset_Handler no cabeçalho do arquivo .Elf :
arm-none-eabi-objdump -t -f stm32-boot-explained.elf | grep " start address "
start address 0x080006e9 Olhando para a nossa tabela de símbolos, Reset_Handler está presente na seção Flash .text , assim como todas as outras funções:
080006e8 g F . text 00000064 Reset_HandlerOs endereços estão alinhados à página, e é por isso que existe a possibilidade de uma incompatibilidade entre o cabeçalho .Elf e o endereço real.
Embora essas informações sejam usadas principalmente pelo ligante para verificar a existência do símbolo do ponto de entrada dentro do código, elas não têm significado prático para o MCU.
De acordo com a especificação STM32, a CPU busca o valor de _estack de primeira linha do endereço 0x00000000 e depois inicia a execução do código a partir da memória de inicialização a partir de 0x00000004 .
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + É exatamente aqui que a memória de alias mencionada acima é definida. Com a configuração padrão, quando o valor do PIN BOOT0 = 0 , ele alias no bloco de memória flash começando em 0x8000000 .
Outras opções baseadas na BOOT0 e BOOT1 incluem memória do sistema com um carregador de inicialização incorporado ou memória RAM. O carregador de inicialização incorporado é programado pelo ST durante a produção e fora do escopo deste manual.
Mas Reset_Handler possui um endereço 0x08000524 que não é exatamente o começo da memória flash, como o MCU encontra o método de bootstrap então?
Aqui é onde a tabela vetorial entra em jogo.
08000000 g O . isr_vector 000001 c4 Vector_Table O MCU trata o início da memória como uma tabela vetorial, que contém ponteiros para várias rotinas de serviço de interrupção e funções de inicialização essenciais, incluindo o Reset_Handler . Consulte as especificações para ver a estrutura exata da tabela que o MCU espera carregar de 0x00000000 . A tabela real deve ser preenchida pelo carregador de inicialização.
| Endereço | Nome |
|---|---|
| 0x00000000 | Reservado |
| 0x00000004 | Redefinir manipulador |
| 0x00000008 | Interrupção não mascarável |
| 0x00000012 | Falha difícil |
| ... | Outras interrupções |
Esta Reset_Handler é uma função de carregador de inicialização que pode ser usada para muitos aplicativos, desde tarefas específicas de segurança até a atualização automática do firmware. Aqui, exploraremos a implementação padrão básica para entender sua interação com a memória do MCU.
Por padrão, o método Reset_Handler é definido no arquivo startup_stm32f436xx.s ASM fornecido pelo STM23CUBEMX. A implementação real do bootloader.c neste projeto está escrita em C para maior clareza.
Observe que as variáveis definidas no script do vinculador podem ser acessadas no código C:
extern uint32_t _estack ;Para que seja facilmente possível replicar a versão ASM.
O processo de carregamento mínimo pode ser dividido nas etapas a seguir:
SystemInit() ).data do Flash para Ram.bss__libc_init_array() )main() )Para as etapas 1 e 4, o STM32CUBEMX fornece implementações de funções, você pode verificar os detalhes no System_StM32F4XX.C.
O projeto possui um conjunto mínimo de arquivos necessários para inicializar o STM32. Você pode tentar verificar você mesmo para verificar a saída do ARM-None-eaBi-objdump e passar com o GDB .
A cadeia de ferramentas do ARM GNU é necessária para construir o projeto.
Recomenda-se instalar um pacote de ferramentas de comando sTM32CUBCLT STM32CUBLT com ferramentas ARM---EABI-GCC , STM32_PROGRAMMAM_CLI e ST-LINK_GDBSERVER incluídas.
Construa o projeto usando cmake :
mkdir build ; cd build
cmake ../ -DPROGRAMMER_CLI=/opt/ST/STM32CubeCLT_1.15.1/STM32CubeProgrammer/bin
-DGDB_SERVER=/opt/ST/STM32CubeCLT_1.15.1/STLink-gdb-server/bin
make VERBOSE=1Observe que o último comando é o estágio de ligação. Se tirarmos todos os outros sinalizadores do compilador, o comando poderá parecer o seguinte. É aqui que o vinculador é instruído a usar nosso script de ligação.
arm-none-eabi-gcc
...
-T " %SCRIPT_DIR%/STM32F446RETx_FLASH.ld "
...
" %OBJ_DIR%/%OBJECT_NAME%.c.obj "
...
-o stm32-boot-explained.elfSTM32_PROGRAMMAM_CLI é pré -configurado para SWD Procotol, basta executar:
make flash Tome uma nota na saída do programador, que está usando o endereço 0x08000000 como ponto de partida:
...
Memory Programming ...
Opening and parsing file: stm32-boot-explained.elf
File : stm32-boot-explained.elf
Size : 1,46 KB
Address : 0x08000000
...Na verdade, é o endereço inicial da memória flash, que já conhecemos.
Existe um destino personalizado pré-configurado para executar o st-link_gdbServer :
# start ST-Link gdb server
make gdb-server
# connect with gdb debugger
gdb -ex ' target remote localhost:61234 ' ./stm32-boot-explained.elf