In che modo un microcontrollore si avvia e si avvia, passo dopo passo?


17

Quando il codice C viene scritto, compilato e caricato su un microcontrollore, il microcontrollore inizia a funzionare. Ma se seguiamo questo processo di upload e avvio passo dopo passo al rallentatore, ho alcune confusioni su ciò che sta effettivamente accadendo all'interno della MCU (memoria, CPU, bootloader). Ecco (molto probabilmente sbagliato) cosa risponderei se qualcuno mi chiedesse:

  1. Il codice binario compilato viene scritto nella ROM flash (o EEPROM) tramite USB
  2. Bootloader copia una parte di questo codice nella RAM. Se vero, come fa il bootloader a sapere cosa copiare (quale parte della ROM copiare nella RAM)?
  3. La CPU inizia a recuperare istruzioni e dati del codice dalla ROM e dalla RAM

È sbagliato?

È possibile riassumere questo processo di avvio e avvio con alcune informazioni su come la memoria, il bootloader e la CPU interagiscono in questa fase?

Ho trovato molte spiegazioni di base su come un PC si avvia tramite BIOS. Ma sono bloccato con il processo di avvio del microcontrollore.

Risposte:


31

1) il file binario compilato viene scritto su prom / flash sì. USB, seriale, i2c, jtag, ecc. Dipendono dal dispositivo da ciò che è supportato da quel dispositivo, irrilevante per comprendere il processo di avvio.

2) Questo in genere non è vero per un microcontrollore, il caso d'uso principale è avere istruzioni in rom / flash e dati in ram. Non importa quale sia l'architettura. per un non microcontrollore, il tuo pc, il tuo laptop, il tuo server, il programma viene copiato da non volatile (disco) in ram e quindi eseguito da lì. Alcuni microcontrollori ti consentono di usare anche ram, anche quelli che dichiarano harvard anche se sembra violare la definizione. Non c'è nulla in Harvard che ti impedisce di mappare ram nel lato istruzioni, devi solo avere un meccanismo per ottenere le istruzioni lì dopo l'accensione (il che viola la definizione, ma i sistemi Harvard dovrebbero farlo per essere utili altri che come microcontrollori).

3) sorta di.

Ogni CPU "si avvia" in modo deterministico, come progettato. Il modo più comune è una tabella vettoriale in cui l'indirizzo per le prime istruzioni da eseguire dopo l'accensione si trovano nel vettore di ripristino, un indirizzo che l'hardware legge quindi utilizza quell'indirizzo per iniziare l'esecuzione. L'altro modo generale è far iniziare l'esecuzione del processore senza una tabella vettoriale in un indirizzo ben noto. A volte il chip avrà "cinturini", alcuni pin che è possibile collegare in alto o in basso prima di rilasciare il reset, che la logica utilizza per l'avvio in modi diversi. Devi separare la cpu stessa, il core del processore dal resto del sistema. Comprendere come funziona la CPU, quindi capire che i progettisti di chip / sistema hanno decodificatori di indirizzi di installazione all'esterno della CPU in modo che una parte dello spazio degli indirizzi della CPU comunichi con un flash, e alcuni con ram e alcuni con periferiche (uart, i2c, spi, gpio, ecc.). Se lo desideri, puoi prendere lo stesso core della CPU e avvolgerlo in modo diverso. Questo è ciò che ottieni quando acquisti qualcosa basato su braccio o mips. arm e mips creano core in cpu, che i chip comprano e avvolgono le proprie cose, per vari motivi che non rendono compatibili quelle cose da marchio a marchio. Ecco perché raramente è possibile porre una domanda generica sul braccio quando si tratta di qualcosa al di fuori del nucleo.

Un microcontrollore tenta di essere un sistema su un chip, quindi la sua memoria non volatile (flash / rom), volatile (sram) e cpu sono tutti sullo stesso chip insieme a una combinazione di periferiche. Ma il chip è progettato internamente in modo tale che il flash sia mappato nello spazio degli indirizzi della CPU che corrisponda alle caratteristiche di avvio di quella CPU. Se ad esempio la cpu ha un vettore di reset all'indirizzo 0xFFFC, allora ci deve essere flash / rom che risponde a quell'indirizzo che possiamo programmare tramite 1), insieme a abbastanza flash / rom nello spazio degli indirizzi per programmi utili. Un progettista di chip può scegliere di avere 0x1000 byte di flash a partire da 0xF000 per soddisfare tali requisiti. E forse hanno messo una certa quantità di RAM ad un indirizzo inferiore o forse 0x0000, e le periferiche da qualche parte nel mezzo.

Un'altra architettura di cpu potrebbe iniziare a essere eseguita all'indirizzo zero, quindi dovrebbero fare il contrario, posizionare il flash in modo che risponda a un intervallo di indirizzi attorno allo zero. ad esempio, da 0x0000 a 0x0FFF. e poi metti qualche montone altrove.

I progettisti di chip sanno come si avvia la CPU e hanno messo lì spazio di archiviazione non volatile (flash / rom). Spetta quindi alla gente del software scrivere il codice di avvio in modo che corrisponda al comportamento ben noto di quella CPU. Devi posizionare l'indirizzo del vettore di ripristino nel vettore di ripristino e il codice di avvio all'indirizzo che hai definito nel vettore di ripristino. La toolchain può aiutarti molto qui. a volte, esp con id point and click o altri sandbox possono fare la maggior parte del lavoro per te, tutto ciò che fai è chiamare API in un linguaggio di alto livello (C).

Tuttavia, tuttavia, il programma caricato nel flash / rom deve corrispondere al comportamento di avvio cablato della cpu. Prima della parte C del programma main () e su se usi main come punto di ingresso, alcune cose devono essere fatte. Il programmatore AC presuppone che quando dichiarano una variabile con un valore iniziale, si aspettano che funzioni effettivamente. Bene, le variabili, oltre a quelle const, sono in ram, ma se ne hai una con un valore iniziale quel valore iniziale deve essere in ram non volatile. Quindi questo è il segmento .data e il bootstrap C deve copiare roba .data da flash a ram (dove di solito è determinato per te dalla toolchain). Si presume che le variabili globali dichiarate senza un valore iniziale siano pari a zero prima dell'avvio del programma, anche se in realtà non si dovrebbe presumere che e per fortuna alcuni compilatori stanno iniziando a mettere in guardia su variabili non inizializzate. Questo è il segmento .bss, e gli zeri di bootstrap C che escono in ram, il contenuto, gli zeri, non devono essere archiviati nella memoria non volatile, ma l'indirizzo iniziale e quanto fa. Ancora una volta la toolchain ti aiuta molto qui. E infine il minimo indispensabile è che è necessario impostare un puntatore dello stack poiché i programmi C prevedono di essere in grado di avere variabili locali e chiamare altre funzioni. Quindi forse vengono fatte altre cose specifiche per chip, o lasciamo che il resto delle cose specifiche per chip accada in C. non deve essere memorizzato nella memoria non volatile, ma l'indirizzo iniziale e quanto. Ancora una volta la toolchain ti aiuta molto qui. E infine il minimo indispensabile è che è necessario impostare un puntatore dello stack poiché i programmi C prevedono di essere in grado di avere variabili locali e chiamare altre funzioni. Quindi forse vengono fatte altre cose specifiche per chip, o lasciamo che il resto delle cose specifiche per chip accada in C. non deve essere memorizzato nella memoria non volatile, ma l'indirizzo iniziale e quanto. Ancora una volta la toolchain ti aiuta molto qui. E infine il minimo indispensabile è che è necessario impostare un puntatore dello stack poiché i programmi C prevedono di essere in grado di avere variabili locali e chiamare altre funzioni. Quindi forse vengono fatte altre cose specifiche per chip, o lasciamo che il resto delle cose specifiche per chip accada in C.

I core della serie cortx-m di arm faranno un po 'di questo per te, il puntatore dello stack si trova nella tabella dei vettori, c'è un vettore di reimpostazione che punta al codice da eseguire dopo il reset, in modo che diverso da quello che devi fare per generare la tabella vettoriale (che di solito usi asm per comunque) puoi passare alla C pura senza asm. ora non riesci a copiare i tuoi .data né a zero i tuoi .bss, quindi devi farlo da solo se vuoi provare ad andare senza asm su qualcosa basato sulla corteccia-m. La funzione più grande non è il vettore di ripristino ma interrompe i vettori in cui l'hardware segue le convenzioni di chiamata C raccomandate dai bracci e conserva i registri per te e utilizza il ritorno corretto per quel vettore, in modo da non dover avvolgere l'asm giusto attorno a ciascun gestore ( o avere direttive specifiche sulla toolchain per il tuo target affinché la toolchain lo avvolga per te).

Potrebbero essere ad esempio elementi specifici del chip, i microcontrollori sono spesso utilizzati nei sistemi basati su batteria, quindi a bassa potenza quindi alcuni escono dal reset con la maggior parte delle periferiche spente e devi accendere ciascuno di questi sottosistemi in modo da poterli usare . Uarts, gpios, ecc. Spesso viene utilizzata una velocità di clock bassa, direttamente da un oscillatore a cristallo o interno. E la progettazione del tuo sistema potrebbe mostrare che hai bisogno di un orologio più veloce, quindi lo inizializzi. l'orologio potrebbe essere troppo veloce per il flash o il ram, quindi potrebbe essere necessario modificare gli stati di attesa prima di aumentare l'orologio. Potrebbe essere necessario configurare uart, o usb o altre interfacce. quindi l'applicazione può fare la sua cosa.

Un desktop di computer, laptop, server e un microcontrollore non sono diversi nel modo in cui si avviano / funzionano. Solo che non si trovano principalmente su un chip. Il programma di bios è spesso su un chip flash / rom separato dalla CPU. Anche se recentemente x86 cpus sta estraendo sempre più di quelli che erano i chip di supporto nello stesso pacchetto (controller per pc, ecc.) Ma hai ancora la maggior parte dei tuoi ram e rom off chip, ma è ancora un sistema e funziona ancora esattamente lo stesso ad alto livello. Il processo di avvio della cpu è ben noto, i progettisti della scheda posizionano il flash / rom nello spazio degli indirizzi in cui la cpu si avvia. quel programma (parte del BIOS su un PC x86) fa tutte le cose sopra menzionate, avvia varie periferiche, inizializza il gioco, enumera i bus dei pcie e così via. Spesso è abbastanza configurabile dall'utente in base alle impostazioni del BIOS o a quelle che chiamavamo impostazioni dei cmos, perché al momento è quella tecnologia utilizzata. Non importa, ci sono impostazioni utente che puoi andare e modificare per dire al codice di avvio del BIOS come variare ciò che fa.

persone diverse useranno una terminologia diversa. si avvia un chip, questo è il primo codice che viene eseguito. a volte chiamato bootstrap. un bootloader con il caricatore di parole spesso significa che se non si fa nulla per interferire, si tratta di un bootstrap che ti porta dall'avvio generico a qualcosa di più grande, l'applicazione o il sistema operativo. ma la parte del caricatore implica che è possibile interrompere il processo di avvio e quindi caricare altri programmi di test. se hai mai usato Uboot per esempio su un sistema Linux incorporato, puoi premere una chiave e fermare il normale avvio quindi puoi scaricare un kernel di prova in ram e avviarlo invece di quello che è in flash, oppure puoi scaricare il tuo propri programmi, oppure è possibile scaricare il nuovo kernel, quindi fare in modo che il bootloader lo scriva in flash in modo che al prossimo avvio esegua il nuovo materiale.

Per quanto riguarda la CPU stessa, il core processor, che non conosce il ram dal flash delle periferiche. Non esiste alcuna nozione di bootloader, sistema operativo, applicazione. È solo una sequenza di istruzioni che vengono inserite nella CPU da eseguire. Questi sono i termini del software per distinguere tra loro diverse attività di programmazione. Concetti di software l'uno dall'altro.

Alcuni microcontrollori hanno un bootloader separato fornito dal fornitore del chip in un flash separato o in un'area separata del flash che potresti non essere in grado di modificare. In questo caso c'è spesso un pin o un set di pin (li chiamo cinturini) che se li leghi in alto o in basso prima di rilasciare il reset stai dicendo alla logica e / o al bootloader cosa fare, ad esempio una combinazione di cinturini può dire al chip di eseguire quel bootloader e attendere in uart la programmazione dei dati nel flash. Impostare le cinghie dall'altra parte e il programma non avvia il bootloader dei produttori di chip, consentendo la programmazione sul campo del chip o il ripristino dal crash del programma. A volte è solo la pura logica che ti permette di programmare il flash. Questo è abbastanza comune in questi giorni,

Il motivo per cui la maggior parte dei microcontrollori ha molta più memoria flash rispetto alla RAM è che il caso d'uso principale è quello di eseguire il programma direttamente dalla memoria flash e avere RAM sufficiente per coprire stack e variabili. Anche se in alcuni casi è possibile eseguire programmi da ram che è necessario compilare correttamente e archiviare in flash, quindi copiare prima di chiamare.

MODIFICARE

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
}

Quindi questo è un esempio di corteccia-m0, la corteccia-ms funziona allo stesso modo per quanto riguarda questo esempio. Il chip particolare, per questo esempio, ha l'applicazione flash all'indirizzo 0x00000000 nello spazio degli indirizzi arm e ram a 0x20000000.

Il modo in cui un cortex-m si avvia è la parola a 32 bit all'indirizzo 0x0000 è l'indirizzo per inizializzare il puntatore dello stack. Non ho bisogno di molto stack per questo esempio, quindi 0x20001000 sarà sufficiente, ovviamente ci deve essere una ram sotto quell'indirizzo (il modo in cui il braccio spinge, viene sottratto prima quindi spinge quindi se si imposta 0x20001000 il primo elemento nello stack è all'indirizzo 0x2000FFFC non è necessario utilizzare 0x2000FFFC). La parola a 32 bit all'indirizzo 0x0004 è l'indirizzo del gestore di reset, in pratica il primo codice che viene eseguito dopo un reset. Poi ci sono più gestori di interrupt ed eventi specifici di quel core e chip di corteccia m, possibilmente fino a 128 o 256, se non li usi, non hai bisogno di impostare la tabella per loro, ne ho lanciati alcuni per dimostrazione scopi.

Non ho bisogno di trattare con .data né .bss in questo esempio perché so già che non c'è nulla in quei segmenti guardando il codice. Se ci fosse, me ne occuperei, e lo farò in un secondo.

Quindi lo stack è impostato, controlla, .data curato, check, .bss, check, quindi le cose C bootstrap sono fatte, possono diramarsi alla funzione entry per C. Perché alcuni compilatori aggiungeranno junk extra se vedono la funzione main () e sulla strada per main, non uso quel nome esatto, ho usato notmain () qui come punto di ingresso C. Quindi il gestore di reset chiama notmain () quindi se / quando notmain () ritorna va in blocco che è solo un ciclo infinito, probabilmente mal chiamato.

Credo fermamente nel padroneggiare gli strumenti, molte persone non lo fanno, ma quello che scoprirai è che ogni sviluppatore bare metal fa le sue cose, a causa della libertà quasi completa, non lontanamente vincolata come faresti per creare app o pagine web . Fanno di nuovo le loro cose. Preferisco avere il mio codice bootstrap e script linker. Altri si affidano alla toolchain o giocano nella sandbox dei fornitori in cui la maggior parte del lavoro viene eseguita da qualcun altro (e se qualcosa si rompe sei in un mondo ferito, e con le parti metalliche nude si rompono spesso e in modo drammatico).

Quindi assemblando, compilando e collegando con strumenti gnu ottengo:

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

Quindi, come fa il bootloader a sapere dove sono le cose. Perché il compilatore ha fatto il lavoro. Nel primo caso l'assemblatore ha generato il codice per flash.s, e così facendo sa dove si trovano le etichette (le etichette sono solo indirizzi proprio come i nomi delle funzioni o i nomi delle variabili, ecc.), Quindi non ho dovuto contare i byte e compilare il vettore tabella manualmente, ho usato un nome di etichetta e l'assemblatore l'ha fatto per me. Ora chiedi, se reset è l'indirizzo 0x14 perché l'assemblatore ha inserito 0x15 nella tabella vettoriale. Bene, questa è una corteccia-m e si avvia e funziona solo in modalità pollice. Con ARM quando si dirama verso un indirizzo se si ramifica in modalità pollice, è necessario impostare lsbit, se si ripristina la modalità arm. Quindi hai sempre bisogno di quel bit impostato. Conosco gli strumenti e mettendo .thumb_func prima di un'etichetta, se quell'etichetta viene utilizzata così com'è nella tabella vettoriale o per ramificarsi o altro. La toolchain sa come impostare lsbit. Quindi ha qui 0x14 | 1 = 0x15. Allo stesso modo per appendere. Ora il disassemblatore non mostra 0x1D per la chiamata a notmain () ma non ti preoccupare, gli strumenti hanno costruito correttamente l'istruzione.

Ora, quel codice in notmain, quelle variabili locali non sono usate, sono codice morto. Il compilatore commenta anche questo fatto dicendo che y è impostato ma non utilizzato.

Nota lo spazio degli indirizzi, tutte queste cose iniziano all'indirizzo 0x0000 e vanno da lì in modo che la tabella vettoriale sia posizionata correttamente, anche lo spazio .text o del programma sia posizionato correttamente, come ho flash.s davanti al codice di notmain.c conoscendo gli strumenti, un errore comune è quello di non farlo bene, di schiantarsi e bruciare duramente. IMO devi disassemblare per assicurarti che le cose siano posizionate proprio prima di avviare la prima volta, una volta che hai le cose nel posto giusto che non devi necessariamente controllare ogni volta. Solo per nuovi progetti o se si bloccano.

Ora qualcosa che sorprende alcune persone è che non c'è motivo di aspettarsi che due compilatori producano lo stesso output dallo stesso input. O anche lo stesso compilatore con impostazioni diverse. Usando clang, il compilatore llvm ottengo questi due output con e senza ottimizzazione

llvm / clang ottimizzato

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

non ottimizzato

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

quindi è una bugia che il compilatore ha ottimizzato l'aggiunta, ma ha allocato due elementi nello stack per le variabili, poiché si tratta di variabili locali che sono in ram ma sullo stack non a indirizzi fissi, vedrà con i globali che quello i cambiamenti. Ma il compilatore si è reso conto che poteva calcolare y in fase di compilazione e non c'era motivo di calcolarlo in fase di esecuzione, quindi ha semplicemente inserito un 1 nello spazio dello stack allocato per xe un 2 per lo spazio dello stack allocato per y. il compilatore "alloca" questo spazio con tabelle interne Dichiaro stack più 0 per la variabile y e stack più 4 per la variabile x. il compilatore può fare quello che vuole fintanto che il codice che implementa è conforme allo standard C o alle aspettative di un programmatore C. Non vi è alcun motivo per cui il compilatore debba lasciare x nello stack + 4 per la durata della funzione,

Se aggiungo una funzione fittizia in assembler

.thumb_func
.globl dummy
dummy:
    bx lr

e poi chiamalo

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

l'uscita cambia

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

ora che abbiamo funzioni nidificate, la funzione notmain deve preservare il suo indirizzo di ritorno, in modo da poter bloccare l'indirizzo di ritorno per la chiamata nidificata. questo perché il braccio usa un registro per i ritorni, se usasse lo stack come dire un x86 o alcuni altri bene ... avrebbe comunque usato lo stack ma in modo diverso. Ora chiedi perché ha spinto r4? Bene, la convenzione di chiamata non molto tempo fa è cambiata per mantenere lo stack allineato su limiti a 64 bit (due parole) anziché a 32 bit, limiti di una parola. Quindi devono spingere qualcosa per mantenere lo stack allineato, quindi il compilatore ha scelto arbitrariamente r4 per qualche motivo, non importa perché. Fare una pausa in r4 sarebbe un bug sebbene secondo la convenzione di chiamata per questo obiettivo, non ostruiamo r4 su una chiamata di funzione, possiamo ostruire da r0 a r3. r0 è il valore restituito. Sembra che stia facendo un'ottimizzazione della coda, forse

Ma vediamo che la matematica xey è ottimizzata per un valore hardcoded di 2 che viene passato alla funzione fittizia (fittizio era specificamente codificato in un file separato, in questo caso asm, in modo che il compilatore non ottimizzasse completamente la funzione, se avessi una funzione fittizia che è semplicemente ritornata in C in notmain.c l'ottimizzatore avrebbe rimosso la chiamata x, ye la funzione fittizia perché sono tutti codici morti / inutili).

Nota anche che, poiché il codice flash.s è diventato più grande, notmain è altrimenti e la toolchain si è occupata della correzione di tutti gli indirizzi per noi, quindi non dobbiamo farlo manualmente.

clang non ottimizzato per riferimento

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 ottimizzato

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}

quell'autore del compilatore ha scelto di usare r7 come variabile fittizia per allineare lo stack, inoltre sta creando un puntatore a frame usando r7 anche se non ha nulla nel frame dello stack. sostanzialmente le istruzioni avrebbero potuto essere ottimizzate. ma ha usato il pop per non restituire tre istruzioni, che probabilmente era su di me Scommetto che avrei potuto ottenere gcc per farlo con le giuste opzioni della riga di comando (specificando il processore).

questo dovrebbe rispondere principalmente al resto delle tue domande

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

Adesso ho i globi. quindi vanno in .data o .bss se non vengono ottimizzati.

prima di guardare l'output finale, guardiamo l'oggetto 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

ora mancano informazioni da questo, ma danno un'idea di ciò che sta succedendo, il linker è quello che prende gli oggetti e li collega insieme alle informazioni fornite (in questo caso flash.ld) che le dice dove .text e. dati e così via. il compilatore non conosce queste cose, può solo concentrarsi sul codice che viene presentato, qualsiasi esterno deve lasciare un buco affinché il linker riempia la connessione. Tutti i dati che deve lasciare un modo per collegare queste cose insieme, quindi gli indirizzi per tutto sono basati su zero qui semplicemente perché il compilatore e questo disassemblatore non lo sanno. ci sono altre informazioni non mostrate qui che il linker usa per posizionare le cose. il codice qui è abbastanza indipendente dalla posizione in modo che il linker possa fare il suo lavoro.

vediamo quindi almeno uno smontaggio dell'output collegato

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

il compilatore ha sostanzialmente chiesto due variabili a 32 bit in ram. Uno è in .bss perché non l'ho inizializzato, quindi si presume che init sia zero. l'altro è .data perché l'ho inizializzato su dichiarazione.

Ora, poiché si tratta di variabili globali, si presume che altre funzioni possano modificarle. il compilatore non fa ipotesi su quando può essere chiamato notmain, quindi non può ottimizzare con ciò che può vedere, la matematica y = x + 1, quindi deve fare quel runtime. Deve leggere da ram le due variabili aggiungerle e salvarle.

Ora chiaramente questo codice non funzionerà. Perché? perché il mio bootstrap come mostrato qui non prepara il ram prima di chiamare notmain, quindi qualunque spazzatura fosse in 0x20000000 e 0x20000004 quando il chip si è svegliato sarà quello che verrà usato per y e x.

Non lo mostrerò qui. puoi leggere il mio divagare ancora più prolisso su .data e .bss e perché non ne ho mai bisogno nel mio codice bare metal, ma se senti che devi e vuoi padroneggiare gli strumenti piuttosto che sperare che qualcun altro lo abbia fatto bene .. .

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

gli script dei linker e i bootstrap sono in qualche modo specifici del compilatore, quindi tutto ciò che apprendi su una versione di un compilatore potrebbe essere lanciato sulla versione successiva o con qualche altro compilatore, un altro motivo per cui non mi sforzo molto nella preparazione di .data e .bss solo per essere così pigro:

unsigned int x=1;

Preferirei di gran lunga fare questo

unsigned int x;
...
x = 1;

e lascia che il compilatore lo metta in .text per me. A volte salva il flash in questo modo a volte brucia di più. È sicuramente molto più facile programmare e trasferire dalla versione della toolchain o da un compilatore all'altro. Molto più affidabile, meno soggetto a errori. Sì, non è conforme allo standard C.

ora cosa succede se creiamo questi globi statici?

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

bene

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

ovviamente quelle variabili non possono essere modificate da altri codici, quindi il compilatore può ora in fase di compilazione ottimizzare il codice morto, come prima.

non ottimizzato

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

questo compilatore che utilizzava lo stack per i locali, ora utilizza ram per i globali e questo codice come scritto è rotto perché non ho gestito correttamente .data né .bss.

e un'ultima cosa che non possiamo vedere nello smontaggio.

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

Ho cambiato x per essere pre-init con 0x12345678. Il mio script per il linker (questo è per gnu ld) ha questa storia su Bob. che dice al linker che voglio che l'ultimo posto sia nello spazio degli indirizzi di ted, ma memorizzalo nel binario nello spazio degli indirizzi di ted e qualcuno lo sposterà per te. E possiamo vedere che è successo. questo è intel formato esadecimale. e possiamo vedere lo 0x12345678

:0400480078563412A0

è nello spazio degli indirizzi flash del binario.

anche questo mostra questo

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

la riga LOAD in cui l'indirizzo virtuale è 0x20000004 e il fisico è 0x48


all'inizio ho due immagini
sfocate

1.) "il caso d'uso primario è avere istruzioni in rom / flash e dati in ram." quando dici "dati nella RAM qui", intendi i dati generati durante il processo del programma. o includi anche i dati inizializzati. intendo quando cariciamo il codice sulla ROM, ci sono già dati inizializzati nel nostro codice. per esempio nel nostro oode se abbiamo: int x = 1; int y = x +1; nel codice sopra ci sono le istruzioni e c'è un dato iniziale che è 1. (x = 1). questi dati vengono copiati anche nella RAM o rimangono solo nella ROM.
user16307

13
ora conosco il limite di caratteri per una risposta di scambio in pila!
old_timer

2
Dovresti scrivere un libro che spieghi questi concetti ai neofiti. "Ho un milione di esempi su github" - È possibile condividere alcuni esempi
AkshayImmanuelD

1
L'ho appena fatto. Non uno che fa qualcosa di utile, ma è comunque un esempio di codice per un microcontrollore. E ho messo un link github dal quale puoi trovare tutto il resto che ho condiviso, buono, cattivo o altro.
old_timer

8

Questa risposta si concentrerà maggiormente sul processo di avvio. Innanzitutto, una correzione - le scritture su flash vengono eseguite dopo che l'MCU (o almeno parte di essa) è già stato avviato. Su alcuni MCU (di solito quelli più avanzati), la CPU stessa potrebbe far funzionare le porte seriali e scrivere sui registri flash. Quindi la scrittura e l'esecuzione del programma sono processi diversi. Presumo che il programma sia già stato scritto in flash.

Ecco il processo di avvio di base. Chiamerò alcune varianti comuni, ma soprattutto lo mantengo semplice.

  1. Ripristino: esistono due tipi di base. Il primo è un reset all'accensione, generato internamente mentre le tensioni di alimentazione aumentano. Il secondo è un interruttore a perno esterno. Indipendentemente da ciò, il reset forza tutti i flip-flop nell'MCU a uno stato predeterminato.

  2. Inizializzazione hardware aggiuntiva: potrebbero essere necessari ulteriori cicli di tempo e / o di clock prima che la CPU inizi a funzionare. Ad esempio, nelle MCU TI su cui lavoro, viene caricata una catena di scansione della configurazione interna.

  3. Avvio CPU: la CPU recupera la sua prima istruzione da un indirizzo speciale chiamato vettore di reset. Questo indirizzo viene determinato al momento della progettazione della CPU. Da lì, è solo la normale esecuzione del programma.

    La CPU ripete ripetutamente tre passaggi di base:

    • Recupero: leggere un'istruzione (valore a 8, 16 o 32 bit) dall'indirizzo memorizzato nel registro del contatore programmi (PC), quindi incrementare il PC.
    • Decodifica: converti l'istruzione binaria in un set di valori per i segnali di controllo interno della CPU.
    • Esegui: eseguire le istruzioni: aggiungere due registri, leggere o scrivere in memoria, diramare (cambiare il PC) o altro.

    (In realtà è più complicato di così. Le CPU sono di solito pipeline , il che significa che possono eseguire ciascuno dei passaggi sopra su diverse istruzioni contemporaneamente. Ciascuno dei passaggi sopra può avere più fasi della pipeline. Quindi ci sono pipeline parallele, previsione del ramo e tutte le fantasiose architetture informatiche che rendono tali CPU Intel impiegano un miliardo di transistor per progettare.)

    Potresti chiederti come funziona il recupero. La CPU ha un bus costituito da segnali di indirizzo (out) e dati (in / out). Per eseguire un recupero, la CPU imposta le sue linee di indirizzo sul valore nel contatore del programma, quindi invia un clock sul bus. L'indirizzo è decodificato per abilitare una memoria. La memoria riceve l'orologio e l'indirizzo e inserisce il valore su quell'indirizzo sulle linee dati. La CPU riceve questo valore. Le letture e le scritture di dati sono simili, tranne per il fatto che l'indirizzo proviene dall'istruzione o da un valore in un registro generico, non dal PC.

    Le CPU con architettura von Neumann hanno un singolo bus utilizzato sia per le istruzioni che per i dati. Le CPU con architettura Harvard hanno un bus per le istruzioni e uno per i dati. Nelle MCU reali, entrambi questi bus possono essere collegati agli stessi ricordi, quindi spesso (ma non sempre) è qualcosa di cui non devi preoccuparti.

    Torna al processo di avvio. Dopo il ripristino, il PC viene caricato con un valore iniziale chiamato vettore di ripristino. Questo può essere integrato nell'hardware o (nelle CPU ARM Cortex-M) può essere letto automaticamente dalla memoria. La CPU recupera le istruzioni dal vettore di ripristino e inizia a scorrere ciclicamente i passaggi precedenti. A questo punto, la CPU funziona normalmente.

  4. Caricatore di avvio: spesso è necessario eseguire alcune impostazioni di basso livello per rendere operativo il resto dell'MCU. Ciò può includere cose come la cancellazione di RAM e il caricamento delle impostazioni di trim di produzione per i componenti analogici. Potrebbe esserci anche un'opzione per caricare il codice da una fonte esterna come una porta seriale o memoria esterna. L'MCU può includere una ROM di avvio che contiene un piccolo programma per eseguire queste operazioni. In questo caso, il vettore di ripristino della CPU punta allo spazio degli indirizzi della ROM di avvio. Questo è fondamentalmente un codice normale, è appena fornito dal produttore, quindi non è necessario scriverlo da soli. :-) In un PC, il BIOS è l'equivalente della ROM di avvio.

  5. Impostazione dell'ambiente C: C prevede di avere uno stack (area RAM per la memorizzazione dello stato durante le chiamate di funzione) e posizioni di memoria inizializzate per le variabili globali. Queste sono le sezioni .stack, .data e .bss di cui parla Dwelch. In questo passaggio alle variabili globali inizializzate i loro valori di inizializzazione vengono copiati da Flash a RAM. Le variabili globali non inizializzate hanno indirizzi RAM vicini tra loro, quindi l'intero blocco di memoria può essere inizializzato a zero molto facilmente. Lo stack non ha bisogno di essere inizializzato (anche se può essere) - tutto ciò che devi fare è impostare il registro del puntatore dello stack della CPU in modo che punti a una regione assegnata nella RAM.

  6. Funzione principale : una volta impostato l'ambiente C, il caricatore C chiama la funzione main (). È qui che inizia normalmente il codice dell'applicazione. Se lo desideri, puoi tralasciare la libreria standard, saltare la configurazione dell'ambiente C e scrivere il tuo codice per chiamare main (). Alcuni MCU potrebbero consentire di scrivere il proprio boot loader, quindi è possibile eseguire tutte le impostazioni di basso livello da soli.

Altre cose: molte MCU ti permetteranno di eseguire il codice dalla RAM per prestazioni migliori. Questo è di solito impostato nella configurazione del linker. Il linker assegna due indirizzi a ciascuna funzione: un indirizzo di caricamento , che è il punto in cui il codice viene memorizzato per la prima volta (in genere flash) e un indirizzo , che è l'indirizzo caricato nel PC per eseguire la funzione (flash o RAM). Per eseguire il codice dalla RAM, scrivere il codice per fare in modo che la CPU copi il codice della funzione dal suo indirizzo di caricamento in flash al suo indirizzo di esecuzione nella RAM, quindi chiamare la funzione all'indirizzo di esecuzione. Il linker può definire variabili globali per aiutare in questo. Ma l'esecuzione di codice dalla RAM è facoltativa nelle MCU. Normalmente lo faresti solo se hai davvero bisogno di prestazioni elevate o se vuoi riscrivere il flash.


1

Il tuo sommario è approssimativamente corretto per l' architettura Von Neumann . Il codice iniziale viene in genere caricato nella RAM tramite un bootloader, ma non (in genere) un bootloader software a cui il termine si riferisce comunemente. Questo è normalmente un comportamento "cotto nel silicio". L'esecuzione del codice in questa architettura comporta spesso una memorizzazione nella cache predittiva delle istruzioni dalla ROM in modo tale che il processore massimizzi il tempo di esecuzione del codice e non sia in attesa che il codice venga caricato nella RAM. Ho letto da qualche parte che MSP430 è un esempio di questa architettura.

In un'architettura di Harvard dispositivo , le istruzioni vengono eseguite direttamente dalla ROM mentre si accede alla memoria dati (RAM) tramite un bus separato. In questa architettura, il codice inizia semplicemente a essere eseguito dal vettore di ripristino. PIC24 e dsPIC33 sono esempi di questa architettura.

Per quanto riguarda l'effettivo lancio di bit che avvia questi processi, che può variare da dispositivo a dispositivo e può coinvolgere debugger, JTAG, metodi proprietari, ecc.


Ma stai saltando alcuni punti velocemente. Prendiamolo al rallentatore. Supponiamo che il codice binario "first" sia scritto nella ROM. Ok .. Dopo di ciò scrivi "Accesso alla memoria dati" .... Ma da dove provengono i dati "nella RAM" all'avvio? Viene di nuovo dalla ROM? E se sì, come fa il bootloader a sapere quale parte della ROM verrà scritta nella RAM all'inizio?
user16307

Hai ragione, ho saltato un sacco. Gli altri ragazzi hanno risposte migliori. Sono contento che tu abbia ottenuto quello che stavi cercando.
leggermente
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.