STM32 Parte 2: Inicialização

A programação está dividindo algo grande e impossível em algo pequeno e muito real.


Olá a todos. Para começar, gostaria de agradecer aos moderadores por terem perdido meu primeiro post (nojento) e dizer olá à mamãe! E também gostaria de agradecer a todos os leitores e pessoas que apontaram meus erros e ajudaram a corrigi-los. Faço imediatamente uma reserva de que em russo não escrevi a partir do 6º ano, em geral, não fique com raiva.

STM32 Parte 1: Noções básicas

Então, vamos começar. Em um artigo anterior, passei rapidamente pelos primeiros pontos. Este foi o nosso arquivo startup.c, responsável por 2 vetores (pilha, Reset_Handler) e um pouco sobre o script do vinculador. Hoje vamos complementar nosso código de inicialização, desmontar o linker para peças de reposição e descobrir como tudo funciona.


extern void *_estack;

void Reset_Handler();

void *vectors[] __attribute__((section(".isr_vector"), used)) = {
    &_estack,
    &Reset_Handler
};

void __attribute__((naked, noreturn)) Reset_Handler()
{
    while(1);
}


Se esse código não estiver claro para ninguém, você poderá ler sobre ele aqui (o artigo não é uma fonte, mas tentou explicar).

Agora vamos descobrir o que está faltando aqui e como adicioná-lo. Obviamente, mícrons têm outros vetores. Por exemplo, o vetor Hard_Fault é um vetor chamado ações impróprias com μ, por exemplo, se ele tentar executar uma instrução que o processador não entende, ele pulará para o endereço do vetor Hard_Fault. Existem muitos desses vetores e não é fato que em nosso programa usaremos tudo. Além disso, os vetores de interrupção estão na mesma tabela.

(Para quem ainda não sabe o que é um vetor, isso é um ponteiro para um endereço, também conhecido como "ponteiro").

Complementamos nosso código e depois explicarei o que fiz.

extern void *_estack;

void Reset_Handler();
void Default_Handler();

void NMI_Handler()   __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler()   __attribute__((weak, alias("Default_Handler")));
//     

void *vectors[] __attribute__((section(".isr_vector"), used)) = {
    &_estack,
    &Reset_Handler
    &NMI_Handler,
    &HardFault_Handler
};

void __attribute__((naked, noreturn)) Reset_Handler()
{
    while(1);
}

void __attribute__((naked, noreturn)) Default_Handler(){
    while(1);
}


Aqui eu adicionei uma nova função Default_Handler (); que irá lidar com todas as interrupções, absolutamente. Mas também com a ajuda, __attribute__((weak, alias("Default_Handler")));observei que se uma função com o mesmo nome for declarada, ela será a manipuladora dessa interrupção. Em outras palavras, agora é apenas um apelido para Default_Handler. Portanto, podemos adicionar todos os vetores na lista do seu manual de referência. Isso é feito para que, se você de repente decidiu usar uma interrupção, mas se esqueceu de criar um manipulador de funções, o Default_Handler usual seja chamado.

Eu acho que neste momento com vetores você pode finalizar e ir para a inicialização de setores e para o vinculador. Para começar, vamos atualizar nosso startup.c novamente adicionando a inicialização dos setores de dados e bss ao corpo Reset_Handler, além de várias variáveis ​​externas.

extern void *_sidata, *_sdata, *_edata, *_sbss, *_ebss;

void __attribute__((naked, noreturn)) Reset_Handler()
{


	void **pSource, **pDest;
	for (pSource = &_sidata, pDest = &_sdata; pDest != &_edata; pSource++, pDest++)
		*pDest = *pSource;

	for (pDest = &_sbss; pDest != &_ebss; pDest++)
		*pDest = 0;

	while(1);
}


Então, novas variáveis ​​e novamente elas foram declaradas em nosso script vinculador. Lembre-se do script anterior . E também compare com um novo.

MEMORY{
	ROM(rx) : ORIGIN = 0x08000000, LENGTH = 1M
	SRAM (rwx):     ORIGIN = 0x20010000, LENGTH = 240K
}

_estack = LENGTH(SRAM) + ORIGIN(SRAM);

SECTIONS{
	.isr_vector : {
	KEEP(*(.isr_vector))
	} >ROM
	
	.text : {
        . = ALIGN(4);
        *(.text)
    } >ROM
	
	_sidata = LOADADDR(.data);
        .data : {
		. = ALIGN(4);
		_sdata = .;
		*(.data)
		. = ALIGN(4);
		_edata = .;
	} >SRAM AT>ROM
	
	.bss : {
		. = ALIGN(4);
		_sbss = .;
		*(.bss)
		. = ALIGN(4);
		_ebss = .;
	} >SRAM
}


Duas novas seções foram adicionadas, essas são .data e .bss. Por que nós precisamos deles? bem .data deixará variáveis ​​globais e estática irá para bss. Ambas as seções estão na RAM e, se tudo é simples com a seção bss (são apenas zeros), existem algumas dificuldades com a seção de dados. Primeiro, os dados já são dados inicializados que são conhecidos em tempo de compilação. Assim, inicialmente a seção de dados estará em algum lugar da memória flash. Em segundo lugar, precisamos indicar de alguma forma ao vinculador o script que ele está na memória flash, mas será transferido para a RAM durante a execução do programa. Vamos dar uma olhada em uma parte do script onde descrevemos a seção de dados.


        _sidata = LOADADDR(.data);
        .data : {
		. = ALIGN(4);
		_sdata = .;
		*(.data)
		. = ALIGN(4);
		_edata = .;
	} >SRAM AT>ROM


Começa com a declaração de _sidata e a atribuição, no momento, de um significado que não está claro para nós. Em seguida, uma descrição da seção em si começa. Aconselho que você leia sobre a função ALIGN aqui , mas, resumidamente, atribuímos ao nosso ponto (endereço atual) um valor que é mais facilmente aceito pelo nosso processador quando ele tenta carregar dados ou instruções a partir daí.

Também estão incluídos _sdata, _edata e o final da seção. Mas no final da seção, há exatamente o que precisamos.SRAM AT>ROM. Esta linha informa ao nosso vinculador que a seção está na SRAM, mas esta seção é carregada na ROM ou na memória flash. Agora, de volta à nossa variável _sidata e à função LOADADDR. Esta função nos retornará um valor que será igual ao endereço da seção de carregamento. Ou seja, indicamos que a seção foi carregada na memória flash, mas a usamos na RAM e, para transferir essa seção, precisamos conhecer seu endereço na memória flash, que é o que a função LOADADDR retorna. Enquanto isso, as variáveis ​​_sdata e _edata indicam a localização da seção na RAM, dando-nos a oportunidade de carregar a seção de dados na parte correta da memória. Deixe-me lembrá-lo de que é com isso que trata o código de inicialização que complementamos acima.

Por que precisamos desse vinculador e de tais dificuldades com a distribuição de seções de nosso aplicativo? A seção Dados é apenas um pequeno exemplo de por que precisamos ser capazes de colocar e alterar seções na memória. Abaixo está um exemplo da seção de texto que eu uso.

.text (ORIGIN(ROM_ITCM) + SIZEOF(.isr_vector)): {
        . = ALIGN(4);
        *(.text)
    } AT>ROM_AXIM


A essência desse bloco é que eu carrego a seção de texto também na memória flash, mas uso um barramento diferente para acessar as instruções e um acelerador ART (como um cache comum). E para que tudo funcione, eu indiquei o endereço de carga e o endereço durante a execução do código, mas não funcionaria se eu não indicasse que a seção deveria ser deslocada. Existem muitas outras invenções, não mostrarei todas elas.

Nesse estágio, as seções são carregadas onde necessário e inicializadas. nós temos uma base de trabalho para escrever mais código. No próximo artigo, escreveremos nosso primeiro driver e piscaremos um LED.

Obrigado a todos por sua atenção e sucesso em seus empreendimentos.

All Articles