Como um microcontrolador é inicializado e inicializado, passo a passo?

15

Quando o código C é gravado, compilado e carregado em um microcontrolador, o microcontrolador começa a funcionar. Mas se tomarmos esse processo de upload e inicialização passo a passo em câmera lenta, tenho algumas confusões sobre o que realmente está acontecendo dentro do MCU (memória, CPU, carregador de inicialização). Aqui está (provavelmente errado) o que eu responderia se alguém me perguntasse:

  1. O código binário compilado é gravado na ROM flash (ou EEPROM) via USB
  2. O carregador de inicialização copia parte do código para a RAM. Se verdadeiro, como o carregador de inicialização sabe o que copiar (qual parte da ROM copiar para a RAM)?
  3. CPU começa a buscar instruções e dados do código da ROM e RAM

Isso está errado?

É possível resumir esse processo de inicialização e inicialização com algumas informações sobre como a memória, o carregador de inicialização e a CPU interagem nessa fase?

Eu encontrei muitas explicações básicas de como um PC inicializa via BIOS. Mas estou preso ao processo de inicialização do microcontrolador.

user16307
fonte

Respostas:

29

1) o binário compilado é gravado em prom / flash yes. USB, serial, i2c, jtag, etc depende do dispositivo quanto ao que é suportado por esse dispositivo, irrelevante para entender o processo de inicialização.

2) Isso normalmente não é verdade para um microcontrolador, o caso de uso principal é ter instruções em rom / flash e dados em ram. Não importa qual seja a arquitetura. para um não-microcontrolador, seu PC, seu laptop, seu servidor, o programa é copiado de não-volátil (disco) para o ram e depois executado a partir daí. Alguns microcontroladores permitem que você use o ram também, mesmo aqueles que reivindicam harvard, apesar de parecer violar a definição. Não há nada na harvard que o impeça de mapear o ram no lado da instrução; você só precisa de um mecanismo para obter as instruções lá depois que a energia acabar (o que viola a definição, mas os sistemas de harvard precisariam fazer isso para ser útil do que como microcontroladores).

3) mais ou menos.

Cada CPU "inicializa" de maneira determinística, conforme projetada. A maneira mais comum é uma tabela de vetores em que o endereço das primeiras instruções a serem executadas após a inicialização está no vetor de redefinição, um endereço que o hardware lê e usa esse endereço para começar a executar. A outra maneira geral é fazer com que o processador comece a executar sem uma tabela de vetores em um endereço conhecido. Às vezes, o chip tem "tiras", alguns pinos que você pode amarrar alto ou baixo antes de liberar a redefinição, que a lógica usa para inicializar de maneiras diferentes. Você precisa separar a própria CPU, o núcleo do processador, do resto do sistema. Entenda como a CPU opera e, em seguida, entenda que os projetistas de chips / sistemas possuem decodificadores de endereços de configuração na parte externa da CPU, para que parte do espaço de endereços da CPU se comunique com um flash, e alguns com ram e outros com periféricos (uart, i2c, spi, gpio, etc). Você pode pegar o mesmo núcleo da CPU, se desejar, e envolvê-lo de maneira diferente. É isso que você recebe quando compra algo com base em braço ou mips. arm and mips produzem núcleos de CPU, que as pessoas compram e envolvem suas próprias coisas, por várias razões que não as tornam compatíveis de marca para marca. É por isso que raramente é possível fazer uma pergunta genérica no braço quando se trata de algo fora do núcleo.

Um microcontrolador tenta ser um sistema em um chip, de modo que sua memória não volátil (flash / rom), volátil (sram) e cpu estão todos no mesmo chip junto com uma mistura de periféricos. Mas o chip é projetado internamente de modo que o flash seja mapeado no espaço de endereço da CPU que corresponda às características de inicialização dessa CPU. Se, por exemplo, a CPU possui um vetor de redefinição no endereço 0xFFFC, é necessário que haja um flash / rom que responda ao endereço que podemos programar via 1), juntamente com flash / rom suficiente no espaço de endereço para programas úteis. Um designer de chips pode optar por ter 0x1000 bytes de flash começando em 0xF000 para atender a esses requisitos. E talvez eles tenham colocado uma quantidade de RAM em um endereço mais baixo ou talvez 0x0000, e os periféricos em algum lugar no meio.

Outra arquitetura da CPU pode começar a executar no endereço zero; portanto, eles precisam fazer o oposto, colocar o flash para que ele atenda a um intervalo de endereços em torno de zero. diga 0x0000 a 0x0FFF, por exemplo. e depois coloque um pouco de carneiro em outro lugar.

Os projetistas de chips sabem como a CPU inicializa e eles colocaram armazenamento não volátil (flash / rom). Cabe então ao pessoal do software escrever o código de inicialização para corresponder ao comportamento bem conhecido dessa CPU. Você deve colocar o endereço do vetor de redefinição no vetor de redefinição e seu código de inicialização no endereço definido no vetor de redefinição. A cadeia de ferramentas pode ajudá-lo bastante aqui. às vezes, especialmente com ids de apontar e clicar ou outras caixas de proteção, eles podem fazer a maior parte do trabalho para você. Tudo o que você faz é chamar apis em um idioma de alto nível (C).

Porém, no entanto, o programa carregado no flash / rom deve corresponder ao comportamento de inicialização por hardware da CPU. Antes da parte C do seu programa main () e se você usar main como seu ponto de entrada, algumas coisas precisam ser feitas. O programador de CA pressupõe que, quando declarar uma variável com um valor inicial, espera que realmente funcione. Bem, variáveis, além das constantes, estão em ram, mas se você tiver uma com um valor inicial, esse valor inicial deve estar em ram não volátil. Portanto, este é o segmento .data e o bootstrap C precisa copiar o material .data do flash para o ram (onde geralmente é determinado pela cadeia de ferramentas para você). As variáveis ​​globais que você declara sem um valor inicial são consideradas zero antes do início do programa, embora você realmente não deva assumir isso e, felizmente, alguns compiladores estão começando a avisar sobre variáveis ​​não inicializadas. Este é o segmento .bss, e os zeros de autoinicialização C que aparecem no ram, o conteúdo, zeros, não precisam ser armazenados na memória não volátil, mas o endereço inicial e quanto custa. Novamente, a cadeia de ferramentas ajuda muito aqui. E, finalmente, o mínimo é que você precisa configurar um ponteiro de pilha, pois os programas C esperam poder ter variáveis ​​locais e chamar outras funções. Talvez outras coisas específicas do chip sejam feitas, ou deixamos o resto do material específico acontecer em C. não precisa ser armazenado na memória não volátil, mas o endereço inicial e quanto custa. Novamente, a cadeia de ferramentas ajuda muito aqui. E, finalmente, o mínimo é que você precisa configurar um ponteiro de pilha, pois os programas C esperam poder ter variáveis ​​locais e chamar outras funções. Talvez outras coisas específicas do chip sejam feitas, ou deixamos o resto do material específico acontecer em C. não precisa ser armazenado na memória não volátil, mas o endereço inicial e quanto custa. Novamente, a cadeia de ferramentas ajuda muito aqui. E, finalmente, o mínimo é que você precisa configurar um ponteiro de pilha, pois os programas C esperam poder ter variáveis ​​locais e chamar outras funções. Talvez outras coisas específicas do chip sejam feitas, ou deixamos o resto do material específico acontecer em C.

Os núcleos da série córtex-m de arm farão isso por você, o ponteiro da pilha está na tabela de vetores, há um vetor de redefinição para apontar para o código a ser executado após a redefinição, de forma que, além do que você precisa fazer para gerar a tabela vetorial (para a qual você costuma usar asm), você pode usar C puro sem asm. agora você não copia seus dados nem copia seus zeros. Portanto, você mesmo deve fazer isso se quiser tentar ficar sem pensar em algo baseado no córtex-m. O recurso maior não é o vetor de redefinição, mas os vetores de interrupção, onde o hardware segue as convenções de chamada C recomendadas pelos braços e preserva os registros para você, e usa o retorno correto para esse vetor, para que você não precise envolver o asm correto em torno de cada manipulador ( ou tenha diretrizes específicas da cadeia de ferramentas para o seu destino, para que a cadeia de ferramentas a envolva por você).

Os itens específicos dos chips podem ser, por exemplo, microcontroladores geralmente usados ​​em sistemas baseados em bateria, portanto, baixa energia, de modo que alguns saem de redefinição com a maioria dos periféricos desligados, e você precisa ativar cada um desses subsistemas para poder usá-los . Uarts, gpios, etc. Geralmente, é usada uma velocidade de clock baixa, diretamente de um oscilador de cristal ou interno. E o design do seu sistema pode mostrar que você precisa de um relógio mais rápido, então você o inicializa. seu relógio pode ser muito rápido para o flash ou o ram, portanto, pode ser necessário alterar os estados de espera antes de aumentar o relógio. Pode ser necessário configurar o uart, ou usb ou outras interfaces. então seu aplicativo pode fazer suas coisas.

Um desktop de computador, laptop, servidor e um microcontrolador não são diferentes na maneira como inicializam / funcionam. Exceto que eles não estão principalmente em um chip. O programa da bios geralmente está em um chip flash / rom separado da CPU. Embora recentemente os x86 cpus estejam puxando cada vez mais o que costumava ser chips de suporte para o mesmo pacote (controladores pcie, etc.), mas você ainda tem a maior parte do seu chip ram e rom, mas ainda é um sistema e ainda funciona exatamente o mesmo em um nível alto. O processo de inicialização da CPU é bem conhecido, os designers da placa colocam o flash / rom no espaço de endereço onde a CPU é inicializada. esse programa (parte do BIOS em um pc x86) faz todas as coisas mencionadas acima, inicia vários periféricos, inicializa o dram, enumera os barramentos do pcie e assim por diante. Geralmente é bastante configurável pelo usuário com base nas configurações da BIOS ou no que costumávamos chamar de configurações de CMOS, porque na época era a tecnologia usada. Não importa, existem configurações do usuário que você pode alterar e informar ao código de inicialização do BIOS como variar o que ele faz.

pessoas diferentes usarão terminologia diferente. um chip inicializa, esse é o primeiro código que é executado. às vezes chamado de autoinicialização. um carregador de inicialização com o carregador de palavras geralmente significa que, se você não faz nada para interferir, é um bootstrap que o leva da inicialização genérica para algo maior, seu aplicativo ou sistema operacional. mas a parte do carregador implica que você pode interromper o processo de inicialização e, talvez, carregar outros programas de teste. Se você já usou o uboot, por exemplo, em um sistema Linux embutido, pode pressionar uma tecla e interromper a inicialização normal. É possível fazer o download de um kernel de teste no ram e inicializá-lo em vez do que está no flash ou fazer o download do seu kernel. programas próprios ou você pode fazer o download do novo kernel e solicitar que o gerenciador de inicialização o escreva para piscar, para que da próxima vez que você inicialize, execute o novo material.

Quanto ao próprio processador, o processador principal, que não conhece a memória ram do flash dos periféricos. Não há noção de gerenciador de inicialização, sistema operacional, aplicativo. É apenas uma sequência de instruções que são alimentadas no processador para serem executadas. Estes são termos de software para distinguir diferentes tarefas de programação umas das outras. Conceitos de software um do outro.

Alguns microcontroladores têm um gerenciador de inicialização separado fornecido pelo fornecedor do chip em um flash separado ou em uma área separada do flash que talvez você não possa modificar. Nesse caso, geralmente há um pino ou conjunto de pinos (eu os chamo de tiras) que, se você os amarrar alto ou baixo, antes que a redefinição seja liberada, estará dizendo à lógica e / ou ao carregador de inicialização o que fazer, por exemplo, uma combinação de alça pode diga ao chip para executar o gerenciador de inicialização e aguarde o uart para que os dados sejam programados no flash. Defina as correias para o outro lado e o seu programa não inicializa o gerenciador de inicialização dos fornecedores de chips, permitindo a programação em campo do chip ou se recuperando da falha do programa. Às vezes, é pura lógica que permite programar o flash. Isso é bastante comum hoje em dia,

A razão pela qual a maioria dos microcontroladores tem muito mais flash que ram é que o caso de uso principal é executar o programa diretamente a partir do flash e ter apenas ram suficiente para cobrir a pilha e as variáveis. Embora em alguns casos você possa executar programas a partir do ram, os quais você precisa compilar corretamente e armazenar em flash e copiar antes de chamar.

EDITAR

flash.s

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

Portanto, este é um exemplo para um córtex-m0, o córtex-ms todos funcionam da mesma forma no que diz respeito a este exemplo. O chip específico, neste exemplo, tem o flash do aplicativo no endereço 0x00000000 no espaço de endereço do braço e o ram no 0x20000000.

A maneira como um córtex-m é inicializado é a palavra de 32 bits no endereço 0x0000 é o endereço para inicializar o ponteiro da pilha. Eu não preciso de muita pilha para este exemplo, de modo que 0x20001000 será suficiente, obviamente, deve haver ram abaixo desse endereço (da maneira como o braço empurra, subtrai primeiro e depois empurra, então, se você definir 0x20001000, o primeiro item da pilha estará no endereço 0x2000FFFC você não precisa usar 0x2000FFFC). A palavra de 32 bits no endereço 0x0004 é o endereço do manipulador de redefinição, basicamente o primeiro código executado após uma redefinição. Depois, há mais manipuladores de interrupção e evento que são específicos para esse núcleo e chip do córtex m, possivelmente até 128 ou 256, se você não os usa, então não precisa configurar a tabela para eles, eu joguei alguns para demonstração propósitos.

Não preciso lidar com dados nem .bss neste exemplo, porque já sei que não há nada nesses segmentos observando o código. Se houvesse eu iria lidar com isso, e vai em um segundo.

Portanto, a pilha é configurada, checada, .dados resolvidos, checada, .bss, checagem, para que o material de inicialização C seja feito, pode ramificar para a função de entrada para C. Como alguns compiladores adicionarão lixo adicional se virem a função main () e no caminho para main, eu não uso esse nome exato, usei notmain () aqui como meu ponto de entrada C. Portanto, o manipulador de redefinição chama notmain () e, se / quando notmain () retorna, ele desliga, o que é apenas um loop infinito, possivelmente com um nome ruim.

Acredito firmemente em dominar as ferramentas, muitas pessoas não sabem, mas o que você encontrará é que cada desenvolvedor do bare metal faz suas próprias coisas, por causa da liberdade quase completa, não remotamente tão restrita quanto você faria com aplicativos ou páginas da web . Eles novamente fazem suas próprias coisas. Prefiro ter meu próprio código de bootstrap e script vinculador. Outros confiam na cadeia de ferramentas ou jogam na caixa de areia dos fornecedores, onde a maior parte do trabalho é feita por outra pessoa (e se algo quebra, você está em um mundo de mágoa e, com o bare metal, as coisas quebram frequentemente e de maneira dramática).

Então, montando, compilando e vinculando com ferramentas gnu, recebo:

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

Então, como o gerenciador de inicialização sabe onde estão as coisas. Porque o compilador fez o trabalho. No primeiro caso, o assembler gerou o código para flash.s e, ao fazê-lo, sabe onde estão os rótulos (os rótulos são apenas endereços, exatamente como nomes de funções ou nomes de variáveis, etc.), então não precisei contar bytes e preencher o vetor tabela manualmente, usei um nome de etiqueta e o montador fez isso por mim. Agora você pergunta, se redefinir é o endereço 0x14, por que o montador colocou 0x15 na tabela de vetores. Bem, este é um córtex-me inicializa e só roda no modo polegar. Com o ARM, quando você ramifica para um endereço, se ramifica para o modo miniatura, o lsbit precisa ser definido; se o modo armar, redefinir. Então você sempre precisa desse conjunto de bits. Conheço as ferramentas e colocando .thumb_func antes de um rótulo, se esse rótulo for usado como na tabela de vetores ou para ramificação ou o que for. A cadeia de ferramentas sabe definir o lsbit. Portanto, tem aqui 0x14 | 1 = 0x15. Da mesma forma para travar. Agora, o desmontador não mostra 0x1D para a chamada não permanecer (), mas não se preocupe, as ferramentas construíram corretamente a instrução.

Agora que o código não está no domínio, essas variáveis ​​locais não são usadas, são código morto. O compilador até comenta esse fato dizendo que y está definido, mas não usado.

Observe o espaço de endereço, todas essas coisas começam no endereço 0x0000 e partem de lá para que a tabela vetorial seja colocada corretamente, o espaço .text ou o programa também seja colocado corretamente, como obtive o flash.s na frente do código do notmain.c conhecendo as ferramentas, um erro comum é não acertar, travar e queimar com força. Na IMO, você precisa desmontar para garantir que as coisas sejam colocadas antes de inicializar pela primeira vez, depois de colocar as coisas no lugar certo, você não precisa necessariamente verificar todas as vezes. Apenas para novos projetos ou se eles travarem.

Agora, algo que surpreende algumas pessoas é que não há razão alguma para esperar que dois compiladores produzam a mesma saída da mesma entrada. Ou mesmo o mesmo compilador com configurações diferentes. Usando clang, o compilador llvm recebo essas duas saídas com e sem otimização

llvm / clang otimizado

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

não otimizado

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

então é uma mentira que o compilador otimizou a adição, mas alocou dois itens na pilha para as variáveis, uma vez que essas são variáveis ​​locais em que estão no ram, mas na pilha não em endereços fixos, verá com os globais que alterar. Mas o compilador percebeu que ele podia computar y em tempo de compilação e não havia motivo para calculá-lo em tempo de execução; portanto, bastava colocar 1 no espaço de pilha alocado para xe 2 no espaço de pilha alocado para y. o compilador "aloca" esse espaço com tabelas internas. Declaro pilha mais 0 para a variável y e pilha mais 4 para a variável x. o compilador pode fazer o que quiser, desde que o código implementado esteja em conformidade com o padrão C ou com as expetações de um programador C. Não há razão para que o compilador tenha que deixar x na pilha + 4 durante a duração da função,

Se eu adicionar uma função fictícia no assembler

.thumb_func
.globl dummy
dummy:
    bx lr

e depois chame

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

a saída muda

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

agora que temos funções aninhadas, a função notmain precisa preservar seu endereço de retorno, para que possa prejudicar o endereço de retorno da chamada aninhada. isso ocorre porque o braço usa um registrador para retornos, se usasse a pilha como digamos um x86 ou alguns outros bem ... ainda usaria a pilha, mas de maneira diferente. Agora você pergunta por que empurrou r4? Bem, a convenção de chamada não mudou muito tempo atrás para manter a pilha alinhada nos limites de 64 bits (duas palavras), em vez dos limites de 32 bits, uma palavra. Portanto, eles precisam pressionar algo para manter a pilha alinhada; portanto, o compilador escolheu arbitrariamente r4 por algum motivo, não importa o motivo. Entrar no r4 seria um bug, mas de acordo com a convenção de chamada para esse destino, não desbotamos o r4 em uma chamada de função, podemos desobstruir o r0 ao r3. r0 é o valor de retorno. Parece que está fazendo uma otimização de cauda, ​​talvez,

Mas vemos que a matemática xey é otimizada para um valor codificado de 2 sendo passado para a função dummy (dummy foi especificamente codificado em um arquivo separado, neste caso asm, para que o compilador não otimize completamente a chamada de função, se eu tivesse uma função fictícia que simplesmente retornasse em C em notmain.c, o otimizador removeria as chamadas de função x, y e fictícia porque são todos códigos mortos / inúteis).

Observe também que, como o código flash.s ficou maior, o domínio não está mais presente e a cadeia de ferramentas cuidou de corrigir todos os endereços para nós, portanto não precisamos fazer isso manualmente.

clang não otimizado para referência

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

clang otimizado

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

esse autor do compilador optou por usar r7 como a variável dummy para alinhar a pilha; também está criando um ponteiro de quadro usando r7, mesmo que não tenha nada no quadro da pilha. basicamente, a instrução poderia ter sido otimizada. mas ele usou o pop para retornar não três instruções, provavelmente por mim, aposto que eu poderia fazer com que o gcc fizesse isso com as opções de linha de comando corretas (especificando o processador).

isso deve responder principalmente ao restante de suas perguntas

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

Eu tenho globais agora. então eles inserem .data ou .bss se não forem otimizados.

antes de olharmos para a saída final, vamos olhar para o objeto itermediate

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

agora há informações ausentes, mas dá uma idéia do que está acontecendo, o vinculador é aquele que pega os objetos e os vincula às informações fornecidas (neste caso, flash.ld) que informa onde .text e. dados e tal. o compilador não conhece essas coisas, apenas pode se concentrar no código apresentado, qualquer externo que tenha que deixar um buraco para o vinculador preencher a conexão. Todos os dados precisam deixar uma maneira de vincular essas coisas, portanto os endereços para tudo são zero baseados aqui simplesmente porque o compilador e esse desmontador não sabem. há outras informações não mostradas aqui que o vinculador usa para colocar as coisas. o código aqui é independente da posição o suficiente para que o vinculador possa fazer seu trabalho.

então vemos pelo menos uma desmontagem da saída vinculada

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

o compilador basicamente pediu duas variáveis ​​de 32 bits em memória ram. Um está em .bss porque eu não o inicializei, portanto, assume-se que inicie como zero. o outro é .data porque eu o inicializei na declaração.

Agora, como essas são variáveis ​​globais, supõe-se que outras funções possam modificá-las. o compilador não faz suposições sobre quando o domínio principal não pode ser chamado, portanto não pode otimizar com o que pode ver, a matemática y = x + 1, portanto, é necessário executar esse tempo de execução. Ele deve ler do ram as duas variáveis, adicioná-las e salvar de volta.

Agora claramente esse código não funcionará. Por quê? porque meu bootstrap, como mostrado aqui, não prepara o carneiro antes de chamar o domínio principal, portanto, qualquer lixo que esteja em 0x20000000 e 0x20000004 quando o chip for ativado é o que será usado para ye x.

Não vou mostrar isso aqui. você pode ler minhas divagações ainda mais longas sobre .data e .bss e por que eu nunca preciso delas no meu código bare metal, mas se você acha que precisa e deseja dominar as ferramentas, em vez de esperar que outra pessoa faça isso direito .. .

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

scripts de vinculador e bootstraps são um tanto específicos do compilador, de modo que tudo que você aprende sobre uma versão de um compilador possa ser lançado na próxima versão ou com outro compilador, mais uma razão pela qual não dedico muito esforço à preparação de .data e .bss apenas para ser tão preguiçoso:

unsigned int x=1;

Eu prefiro fazer isso

unsigned int x;
...
x = 1;

e deixe o compilador colocá-lo em .text para mim. Às vezes, ele economiza flash dessa maneira, às vezes queima mais. Definitivamente, é muito mais fácil programar e portar da versão da cadeia de ferramentas ou de um compilador para outro. Muito mais confiável, menos propenso a erros. Sim, não está de acordo com o padrão C.

Agora, e se fizermos esses globos estáticos?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

bem

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

obviamente, essas variáveis ​​não podem ser modificadas por outro código; portanto, o compilador agora pode, em tempo de compilação, otimizar o código morto, como antes.

não otimizado

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

esse compilador que usava a pilha para os locais, agora usa ram para globais e esse código, como está escrito, está quebrado porque eu não lidei com .data nem .bss corretamente.

e uma última coisa que não podemos ver na desmontagem.

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

Alterei x para ser pré-inicial com 0x12345678. Meu script de vinculador (isto é para o gnu ld) tem essa coisa ted at bob. isso informa ao vinculador que eu quero que o local final esteja no espaço de endereço ted, mas armazene-o no binário no espaço de endereço ted e alguém o moverá para você. E podemos ver o que aconteceu. este é o formato hexadecimal da intel. e podemos ver o 0x12345678

:0400480078563412A0

está no espaço de endereço flash do binário.

readelf também mostra isso

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

a linha LOAD em que o endereço virtual é 0x20000004 e o físico é 0x48

old_timer
fonte
no início, eu tenho dois imagem borrão de coisas:
user16307
1.) "o caso de uso principal é ter instruções em rom / flash e dados em ram." quando você diz "dados na RAM aqui", você quer dizer os dados gerados no processo do programa. ou você também inclui os dados inicializados. Quero dizer, quando carregamos o código na ROM, já existem dados inicializados em nosso código. por exemplo, em nosso oode, se tivermos: int x = 1; int y = x +1; o código acima contém instruções e há um dado inicial que é 1. (x = 1). Esses dados também são copiados para a RAM ou permanecem apenas na ROM.
user16307
12
agora sei o limite de caracteres para uma resposta de troca de pilhas!
old_timer
2
Você deve escrever um livro explicando esses conceitos para iniciantes. "Eu tenho um zilhão de exemplos no github" - É possível compartilhar alguns exemplos
AlphaGoku 24/16
1
Eu apenas fiz. Não é algo que faz algo útil, mas ainda assim é um exemplo de código para um microcontrolador. E eu coloquei um link no github a partir do qual você pode encontrar tudo o que compartilhei, bom, ruim ou não.
old_timer
8

Esta resposta vai se concentrar mais no processo de inicialização. Primeiro, uma correção - as gravações no flash são feitas depois que o MCU (ou pelo menos parte dele) já foi iniciado. Em alguns MCUs (geralmente os mais avançados), a própria CPU pode operar as portas seriais e gravar nos registradores flash. Portanto, escrever e executar o programa são processos diferentes. Vou assumir que o programa já foi escrito para piscar.

Aqui está o processo básico de inicialização. Vou citar algumas variações comuns, mas principalmente estou mantendo isso simples.

  1. Redefinir: Existem dois tipos básicos. O primeiro é um reset de inicialização, que é gerado internamente enquanto as tensões de alimentação estão aumentando. O segundo é um alternador de pinos externo. Independentemente disso, a redefinição força todos os flip-flops no MCU a um estado predeterminado.

  2. Inicialização extra de hardware: Pode ser necessário tempo extra e / ou ciclos de clock antes que a CPU comece a funcionar. Por exemplo, nas TI MCUs em que trabalho, há uma cadeia de varredura de configuração interna que é carregada.

  3. Inicialização da CPU: A CPU busca sua primeira instrução em um endereço especial chamado vetor de redefinição. Este endereço é determinado quando a CPU é projetada. A partir daí, é apenas a execução normal do programa.

    A CPU repete três etapas básicas repetidas vezes:

    • Buscar: Leia uma instrução (valor de 8, 16 ou 32 bits) do endereço armazenado no registro do contador de programas (PC) e depois aumente o PC.
    • Decodificação: Converta a instrução binária em um conjunto de valores para os sinais de controle interno da CPU.
    • Execute: Execute as instruções - adicione dois registros, leia ou escreva na memória, ramifique (troque o PC) ou o que for.

    (Na verdade, é mais complicado que isso. As CPUs geralmente são canalizadas , o que significa que podem executar cada uma das etapas acima com instruções diferentes ao mesmo tempo. Cada uma das etapas acima pode ter vários estágios de pipeline. Depois, há pipelines paralelos, previsão de ramificação , e todo o material sofisticado de arquitetura de computadores que faz com que esses processadores Intel levem um bilhão de transistores para projetar.)

    Você pode estar se perguntando como a busca funciona. A CPU possui um barramento composto por sinais de endereço (saída) e dados (entrada / saída). Para fazer uma busca, a CPU define suas linhas de endereço com o valor no contador do programa e envia um relógio pelo barramento. O endereço é decodificado para ativar uma memória. A memória recebe o relógio e o endereço e coloca o valor nesse endereço nas linhas de dados. A CPU recebe esse valor. As leituras e gravações de dados são semelhantes, exceto que o endereço vem da instrução ou de um valor em um registro de uso geral, não do PC.

    As CPUs com arquitetura von Neumann têm um único barramento usado para instruções e dados. As CPUs com arquitetura Harvard têm um barramento para instruções e outro para dados. Em MCUs reais, esses dois barramentos podem estar conectados às mesmas memórias; portanto, é frequentemente (mas nem sempre) algo com que você não precisa se preocupar.

    Voltar ao processo de inicialização. Após a redefinição, o PC é carregado com um valor inicial chamado vetor de redefinição. Isso pode ser incorporado ao hardware ou (nas CPUs ARM Cortex-M) pode ser lido automaticamente na memória. A CPU busca a instrução do vetor de redefinição e começa a percorrer as etapas acima. Neste ponto, a CPU está executando normalmente.

  4. Carregador de inicialização: Muitas vezes, é necessário fazer uma configuração de baixo nível para tornar o restante do MCU operacional. Isso pode incluir coisas como limpar RAMs e carregar configurações de acabamento de fabricação para componentes analógicos. Também pode haver uma opção para carregar o código de uma fonte externa, como uma porta serial ou memória externa. O MCU pode incluir uma ROM de inicialização que contém um pequeno programa para fazer essas coisas. Nesse caso, o vetor de redefinição da CPU aponta para o espaço de endereço da ROM de inicialização. Este é basicamente um código normal, é fornecido apenas pelo fabricante para que você não precise escrevê-lo. :-) Em um PC, o BIOS é equivalente à ROM de inicialização.

  5. Configuração do ambiente C: C espera ter uma pilha (área de RAM para armazenamento de estado durante chamadas de função) e localizações de memória inicializadas para variáveis ​​globais. Essas são as seções .stack, .data e .bss das quais Dwelch está falando. As variáveis ​​globais inicializadas têm seus valores de inicialização copiados do flash para a RAM nesta etapa. Variáveis ​​globais não inicializadas têm endereços de RAM próximos, portanto todo o bloco de memória pode ser inicializado para zero com muita facilidade. A pilha não precisa ser inicializada (embora possa ser) - tudo o que você realmente precisa fazer é definir o registro do ponteiro da pilha da CPU para que aponte para uma região atribuída na RAM.

  6. Função principal : Depois que o ambiente C é configurado, o carregador C chama a função main (). É aí que o código do seu aplicativo normalmente começa. Se desejar, você pode deixar de fora a biblioteca padrão, pular a configuração do ambiente C e escrever seu próprio código para chamar main (). Alguns MCUs podem permitir que você escreva seu próprio carregador de inicialização e, em seguida, você pode fazer toda a configuração de baixo nível sozinho.

Coisas diversas: Muitos MCUs permitem executar código fora da RAM para obter melhor desempenho. Isso geralmente é definido na configuração do vinculador. O vinculador atribui dois endereços a todas as funções - um endereço de carregamento , onde o código é armazenado pela primeira vez (normalmente flash) e um endereço de execução , que é o endereço carregado no PC para executar a função (flash ou RAM). Para executar o código fora da RAM, você escreve um código para fazer a CPU copiar o código de função do endereço de carregamento em flash para o endereço de execução na RAM e depois chamar a função no endereço de execução. O vinculador pode definir variáveis ​​globais para ajudar nisso. Mas executar código fora da RAM é opcional nas MCUs. Você normalmente faria isso apenas se realmente precisar de alto desempenho ou se quiser reescrever o flash.

Adam Haun
fonte
1

Seu resumo está aproximadamente correto para a arquitetura Von Neumann . O código inicial normalmente é carregado na RAM por meio de um gerenciador de inicialização, mas não (normalmente) um gerenciador de inicialização de software ao qual o termo normalmente se refere. Normalmente, esse é um comportamento "assado no silício". A execução de código nessa arquitetura geralmente envolve um cache preditivo das instruções da ROM, de forma que o processador maximize seu tempo de execução do código e não aguarde o carregamento do código na RAM. Li em algum lugar que o MSP430 é um exemplo dessa arquitetura.

Em um dispositivo da Harvard Architecture , as instruções são executadas diretamente da ROM enquanto a memória de dados (RAM) é acessada através de um barramento separado. Nessa arquitetura, o código simplesmente começa a executar a partir do vetor de redefinição. O PIC24 e o dsPIC33 são exemplos dessa arquitetura.

Quanto à troca real de bits que inicia esses processos, isso pode variar de dispositivo para dispositivo e pode envolver depuradores, JTAG, métodos proprietários, etc.

ligeiramente
fonte
Mas você está pulando alguns pontos rapidamente. Vamos levá-lo em câmera lenta. Digamos que o código binário "primeiro" seja gravado na ROM. Ok .. Depois que você escreve "A memória de dados é acessada" .... Mas de onde vêm os dados "para a RAM" na inicialização? Vem da ROM novamente? E se sim, como o gerenciador de inicialização sabe qual parte da ROM será gravada na RAM no início?
user16307
Você está correto, eu pulei bastante. Os outros caras têm respostas melhores. Estou feliz que você conseguiu o que estava procurando.
slightlynybbled