Wie bootet und startet ein Mikrocontroller Schritt für Schritt?

15

Wenn C-Code geschrieben, kompiliert und auf einen Mikrocontroller hochgeladen wird, wird der Mikrocontroller gestartet. Aber wenn wir diesen Upload- und Startvorgang Schritt für Schritt in Zeitlupe durchführen, habe ich einige Verwirrungen darüber, was tatsächlich in der MCU passiert (Speicher, CPU, Bootloader). Hier ist (höchstwahrscheinlich falsch), was ich antworten würde, wenn mich jemand fragen würde:

  1. Kompilierter Binärcode wird über USB in das Flash-ROM (oder EEPROM) geschrieben
  2. Der Bootloader kopiert einen Teil dieses Codes in den Arbeitsspeicher. Wenn ja, woher weiß der Bootloader, was er kopieren soll (welchen Teil des ROM in den RAM kopieren soll)?
  3. Die CPU beginnt, Anweisungen und Daten des Codes aus dem ROM und dem RAM abzurufen

Ist das falsch?

Ist es möglich, diesen Vorgang des Bootens und Startens mit einigen Informationen über die Interaktion von Speicher, Bootloader und CPU in dieser Phase zusammenzufassen?

Ich habe viele grundlegende Erklärungen gefunden, wie ein PC über das BIOS bootet. Aber ich bin beim Startvorgang des Mikrocontrollers hängen geblieben.

user16307
quelle

Antworten:

29

1) Die kompilierte Binärdatei wird in prom / flash yes geschrieben. USB, seriell, i2c, jtag usw. hängen von dem Gerät ab, was von diesem Gerät unterstützt wird. Dies ist für das Verständnis des Startvorgangs unerheblich.

2) Dies gilt normalerweise nicht für einen Mikrocontroller. Der primäre Anwendungsfall besteht darin, Anweisungen in ROM / Flash und Daten in RAM zu haben. Egal welche Architektur. Für einen Nicht-Mikrocontroller, Ihren PC, Ihren Laptop, Ihren Server wird das Programm von nichtflüchtig (Festplatte) auf RAM kopiert und von dort ausgeführt. Bei einigen Mikrocontrollern können Sie auch RAM verwenden, auch wenn dies gegen die Definition zu verstoßen scheint. Es gibt nichts an Harvard, was Sie daran hindert, RAM auf die Anweisungsseite zuzuordnen. Sie benötigen lediglich einen Mechanismus, um die Anweisungen nach dem Einschalten abzurufen (was gegen die Definition verstößt, aber Harvard-Systeme müssten dies tun, um andere zu unterstützen als als Mikrocontroller).

3) irgendwie.

Jede CPU "bootet" auf deterministische, wie vorgesehene Weise. Der gebräuchlichste Weg ist eine Vektortabelle, in der sich die Adresse für die ersten Befehle, die nach dem Einschalten ausgeführt werden, im Rücksetzvektor befindet. Diese Adresse wird von der Hardware gelesen und verwendet, um mit der Ausführung zu beginnen. Die andere allgemeine Möglichkeit besteht darin, den Prozessor ohne Vektortabelle an einer bekannten Adresse mit der Ausführung zu beginnen. Manchmal hat der Chip "Bänder", einige Stifte, die Sie hoch oder niedrig binden können, bevor Sie das Zurücksetzen freigeben, das die Logik verwendet, um auf verschiedene Weise zu starten. Sie müssen die CPU selbst, den Prozessorkern, vom Rest des Systems trennen. Verstehen Sie, wie die CPU funktioniert, und stellen Sie dann fest, dass die Chip- / Systementwickler Adressdecoder an der Außenseite der CPU installiert haben, sodass ein Teil des CPU-Adressraums mit einem Flash kommuniziert. und einige mit RAM und einige mit Peripheriegeräten (Uart, I2C, SPI, GPIO, etc.). Sie können denselben CPU-Kern auf Wunsch auch anders verpacken. Das bekommen Sie, wenn Sie etwas kaufen, das auf Arm oder Mips basiert. Arm und Mips stellen CPU-Kerne her, die die Leute kaufen und ihre eigenen Sachen herumwickeln, aus verschiedenen Gründen machen sie diese Sachen nicht kompatibel von Marke zu Marke. Aus diesem Grund kann eine generische Arm-Frage nur selten gestellt werden, wenn es um etwas außerhalb des Kerns geht.

Ein Mikrocontroller versucht, ein System auf einem Chip zu sein. Daher befinden sich sein nichtflüchtiger Speicher (Flash / Rom), sein flüchtiger Speicher (SRAM) und seine CPU zusammen mit einer Mischung aus Peripheriegeräten auf demselben Chip. Der Chip ist jedoch intern so konzipiert, dass der Flash-Speicher in den Adressraum der CPU abgebildet wird, der den Boot-Eigenschaften dieser CPU entspricht. Wenn zum Beispiel die CPU einen Rücksetzvektor an der Adresse 0xFFFC hat, muss es ein Flash / ROM geben, das auf diese Adresse reagiert, die wir über 1) programmieren können, zusammen mit genügend Flash / ROM im Adressraum für nützliche Programme. Ein Chipdesigner kann sich dafür entscheiden, ab 0xF000 0x1000 Flash-Bytes zu haben, um diese Anforderungen zu erfüllen. Und vielleicht haben sie etwas RAM an eine niedrigere Adresse oder vielleicht 0x0000 und die Peripherie irgendwo in der Mitte gelegt.

Eine andere CPU-Architektur wird möglicherweise ab Adresse Null ausgeführt. Sie müssten also das Gegenteil tun und den Flash so platzieren, dass er auf einen Adressbereich um Null antwortet. sagen Sie zum Beispiel 0x0000 bis 0x0FFF. und dann irgendwo anders rammen.

Die Chip-Designer wissen, wie die CPU bootet, und haben dort nichtflüchtigen Speicher (Flash / Rom) abgelegt. Es liegt dann an der Software, den Boot-Code so zu schreiben, dass er dem bekannten Verhalten dieser CPU entspricht. Sie müssen die Rücksetzvektoradresse im Rücksetzvektor und Ihren Startcode an der Adresse platzieren, die Sie im Rücksetzvektor definiert haben. Die Toolchain kann Ihnen hier sehr helfen. manchmal, vor allem mit Point-and-Click-IDs oder anderen Sandboxes, erledigen sie die meiste Arbeit für Sie. Alles, was Sie tun, ist, apis in einer höheren Sprache (C) aufzurufen.

Das in das Flash / Rom geladene Programm muss jedoch dem festverdrahteten Startverhalten der CPU entsprechen. Vor dem C-Teil Ihres Programms main () und wenn Sie main als Einstiegspunkt verwenden, müssen einige Dinge getan werden. Der AC-Programmierer geht davon aus, dass die Deklaration einer Variablen mit einem Anfangswert tatsächlich funktioniert. Nun, andere Variablen als Konstanten sind im RAM, aber wenn Sie eine mit einem Anfangswert haben, muss dieser Anfangswert im nichtflüchtigen RAM sein. Dies ist also das .data-Segment, und der C-Bootstrap muss .data-Dateien von Flash auf RAM kopieren (was normalerweise von der Toolchain festgelegt wird). Globale Variablen, die Sie ohne Anfangswert deklarieren, werden vor dem Start Ihres Programms als Null angenommen, obwohl Sie das eigentlich nicht annehmen sollten. Zum Glück beginnen einige Compiler, vor nicht initialisierten Variablen zu warnen. Dies ist das .bss-Segment, und die C-Bootstrap-Nullen, die im RAM ausgegeben werden, der Inhalt, Nullen, müssen nicht im nichtflüchtigen Speicher gespeichert werden, sondern die Startadresse und wie viel. Auch hier hilft Ihnen die Toolchain sehr. Als letztes müssen Sie einen Stapelzeiger einrichten, da C-Programme erwarten, lokale Variablen zu haben und andere Funktionen aufzurufen. Dann werden vielleicht noch andere chipspezifische Sachen gemacht, oder wir lassen den Rest der chipspezifischen Sachen in C passieren. muss nicht im nichtflüchtigen Speicher abgelegt werden, sondern die Startadresse und wie viel. Auch hier hilft Ihnen die Toolchain sehr. Als letztes müssen Sie einen Stapelzeiger einrichten, da C-Programme erwarten, lokale Variablen zu haben und andere Funktionen aufzurufen. Dann werden vielleicht noch andere chipspezifische Sachen gemacht, oder wir lassen den Rest der chipspezifischen Sachen in C passieren. muss nicht im nichtflüchtigen Speicher abgelegt werden, sondern die Startadresse und wie viel. Auch hier hilft Ihnen die Toolchain sehr. Als letztes müssen Sie einen Stapelzeiger einrichten, da C-Programme erwarten, lokale Variablen zu haben und andere Funktionen aufzurufen. Dann werden vielleicht noch andere chipspezifische Sachen gemacht, oder wir lassen den Rest der chipspezifischen Sachen in C passieren.

Die Kerne der cortex-m-Serie von arm erledigen einen Teil davon für Sie. Der Stapelzeiger befindet sich in der Vektortabelle. Es gibt einen Rücksetzvektor, der auf den Code verweist, der nach dem Zurücksetzen ausgeführt werden soll, sodass Sie nichts weiter tun müssen Um die Vektortabelle zu generieren (für die Sie normalerweise sowieso asm verwenden), können Sie C ohne asm verwenden. Jetzt werden Ihre .data nicht mehr kopiert und Ihre .bss nicht mehr auf Null gesetzt. Sie müssen dies also selbst tun, wenn Sie versuchen möchten, auf etwas Kortex-M-basiertem zu verzichten. Die größere Funktion ist nicht der Rücksetzvektor, sondern die Interruptvektoren, bei denen die Hardware der von Arms empfohlenen C-Aufrufkonvention folgt und die Register für Sie beibehält und die korrekte Rückgabe für diesen Vektor verwendet, damit Sie nicht den richtigen Asm um jeden Handler wickeln müssen ( oder haben Toolchain-spezifische Anweisungen für Ihr Ziel, damit die Toolchain sie für Sie umschließt).

Chipspezifisches Material kann beispielsweise sein, dass in batteriebasierten Systemen häufig Mikrocontroller verwendet werden. Daher ist der Stromverbrauch so gering, dass einige Geräte nach dem Zurücksetzen bei größtenteils ausgeschaltetem Peripheriegerät ausfallen und Sie jedes dieser Subsysteme einschalten müssen, damit Sie sie verwenden können . Uarts, gpios usw. Oft wird eine niedrige Taktrate verwendet, direkt von einem Quarz oder einem internen Oszillator. Und Ihr Systemdesign zeigt möglicherweise, dass Sie eine schnellere Uhr benötigen. Initialisieren Sie diese. Ihre Uhr ist möglicherweise zu schnell für den Blitz oder den RAM, sodass Sie möglicherweise den Wartezustand ändern müssen, bevor Sie die Uhr erhöhen. Möglicherweise müssen Sie den Uart oder USB oder andere Schnittstellen einrichten. dann kann Ihre Anwendung ihre Sache tun.

Ein Computer-Desktop, ein Laptop, ein Server und ein Mikrocontroller unterscheiden sich nicht darin, wie sie starten / arbeiten. Abgesehen davon, dass sie meistens nicht auf einem Chip sind. Das BIOS-Programm befindet sich häufig auf einem separaten Chip-Flash / ROM von der CPU. Zwar ziehen in letzter Zeit x86-CPUs immer mehr der früher unterstützten Chips in dasselbe Paket (PCIE-Controller usw.), aber Sie haben immer noch den größten Teil Ihres RAM- und ROM-Off-Chips, aber es ist immer noch ein System und es funktioniert immer noch genau das gleiche auf hohem niveau. Der CPU-Boot-Prozess ist allgemein bekannt. Die Board-Designer platzieren das Flash / ROM im Adressraum, in dem die CPU bootet. Dieses Programm (Teil des BIOS auf einem x86-PC) erledigt alle oben genannten Aufgaben, startet verschiedene Peripheriegeräte, initialisiert dram, zählt die PCIE-Busse auf und so weiter. Ist für den Benutzer oft recht konfigurierbar, basierend auf den BIOS-Einstellungen oder den so genannten CMOS-Einstellungen. Egal, es gibt Benutzereinstellungen, die Sie ändern können, um dem BIOS-Boot-Code mitzuteilen, wie er sich ändern soll.

Verschiedene Leute verwenden unterschiedliche Terminologie. Ein Chip bootet, das ist der erste Code, der ausgeführt wird. manchmal Bootstrap genannt. Ein Bootloader mit dem Wort Loader bedeutet häufig, dass Sie, wenn Sie nichts tun, um zu stören, einen Bootstrap ausführen, der Sie vom allgemeinen Booten in etwas Größeres, Ihre Anwendung oder Ihr Betriebssystem, abhält. Der Loader-Teil impliziert jedoch, dass Sie den Startvorgang unterbrechen und dann möglicherweise andere Testprogramme laden können. Wenn Sie uboot zum Beispiel jemals auf einem Embedded-Linux-System verwendet haben, können Sie eine Taste drücken und den normalen Startvorgang beenden. Anschließend können Sie einen Testkernel in den RAM-Speicher herunterladen und ihn anstelle des Flash-Kernels starten oder Ihren Kernel herunterladen eigene Programme, oder Sie können den neuen Kernel herunterladen und ihn dann vom Bootloader zum Flashen schreiben lassen, damit er beim nächsten Start das neue Zeug ausführt.

Was die CPU selbst anbelangt, so ist dies der Kernprozessor, der keinen RAM von Flash von Peripheriegeräten kennt. Es gibt keine Vorstellung von Bootloader, Betriebssystem, Anwendung. Es ist nur eine Folge von Befehlen, die in die auszuführende CPU eingegeben werden. Dies sind Software-Begriffe, um verschiedene Programmieraufgaben voneinander zu unterscheiden. Softwarekonzepte voneinander.

Einige Mikrocontroller verfügen über einen separaten Bootloader, der vom Chip-Hersteller in einem separaten Flash oder einem separaten Flash-Bereich bereitgestellt wird, den Sie möglicherweise nicht ändern können. In diesem Fall gibt es oft einen Stift oder eine Reihe von Stiften (ich nenne sie Bänder), die, wenn Sie sie vor dem Zurücksetzen hoch oder niedrig binden, der Logik und / oder dem Bootloader mitteilen, was zu tun ist, z. B. eine Bandkombination Sagen Sie dem Chip, dass er diesen Bootloader ausführen soll, und warten Sie auf dem UART, bis die Daten in den Flash programmiert wurden. Stellen Sie die Gurte in die andere Richtung und Ihr Programm bootet nicht den Bootloader des Chipherstellers, wodurch der Chip vor Ort programmiert oder ein Programmabsturz behoben werden kann. Manchmal ist es nur reine Logik, mit der Sie den Blitz programmieren können. Das ist heutzutage ziemlich üblich,

Der Grund, warum die meisten Mikrocontroller viel mehr Flash als RAM haben, ist, dass der primäre Anwendungsfall darin besteht, das Programm direkt von Flash aus auszuführen und nur genügend RAM zu haben, um Stapel und Variablen abzudecken. Obwohl Sie in einigen Fällen Programme von RAM ausführen können, die Sie direkt kompilieren und in Flash speichern müssen, kopieren Sie diese vor dem Aufruf.

BEARBEITEN

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
}

Dies ist also ein Beispiel für einen Cortex-m0, die Cortex-ms funktionieren nach diesem Beispiel alle gleich. Der spezielle Chip für dieses Beispiel hat einen Anwendungs-Flash bei der Adresse 0x00000000 im Armadressraum und einen RAM bei 0x20000000.

Die Art und Weise, wie ein Cortex-m bootet, ist das 32-Bit-Wort an der Adresse 0x0000, die Adresse zum Initialisieren des Stapelzeigers. Ich brauche nicht viel Stack für dieses Beispiel, also wird 0x20001000 ausreichen. Offensichtlich muss es einen RAM unterhalb dieser Adresse geben (wie der Arm drückt, subtrahiert er zuerst und drückt dann, wenn Sie 0x20001000 einstellen, befindet sich das erste Element auf dem Stack an der Adresse 0x2000FFFC Sie müssen 0x2000FFFC nicht verwenden). Das 32-Bit-Wort an Adresse 0x0004 ist die Adresse für den Rücksetz-Handler, im Grunde der erste Code, der nach einem Zurücksetzen ausgeführt wird. Dann gibt es mehr Interrupt- und Event-Handler, die spezifisch für diesen Cortex-Kern und -Chip sind, möglicherweise bis zu 128 oder 256. Wenn Sie sie nicht verwenden, brauchen Sie die Tabelle nicht für sie einzurichten, ich habe ein paar zur Demonstration reingeworfen Zwecke.

In diesem Beispiel muss ich weder mit .data noch mit .bss umgehen, da ich bereits weiß, dass in diesen Segmenten nichts vorhanden ist, wenn ich den Code betrachte. Wenn ich es gäbe, würde ich mich darum kümmern und werde es in einer Sekunde tun.

Der Stack ist also eingerichtet, geprüft, .data gepflegt, geprüft, .bss, geprüft, damit das C-Bootstrap-Zeug fertig ist, und kann zur Eingabefunktion für C verzweigen. Einige Compiler fügen zusätzlichen Müll hinzu, wenn sie die Funktion sehen main () und auf dem Weg nach main verwende ich nicht genau diesen Namen. Ich habe notmain () hier als C-Einstiegspunkt verwendet. Der Reset-Handler ruft also notmain () auf, und wenn notmain () zurückgibt, bleibt er hängen. Dies ist nur eine Endlosschleife, die möglicherweise einen schlechten Namen hat.

Ich bin fest davon überzeugt, die Tools zu beherrschen, viele Leute tun das nicht, aber Sie werden feststellen, dass jeder Bare-Metal-Entwickler aufgrund der nahezu vollständigen Freiheit sein eigenes Ding macht, das nicht im entferntesten so eingeschränkt ist, als würden Sie Apps oder Webseiten erstellen . Sie machen wieder ihr eigenes Ding. Ich bevorzuge es, meinen eigenen Bootstrap-Code und ein eigenes Linker-Skript zu haben. Andere verlassen sich auf die Toolchain oder spielen in der Vendors Sandbox, in der der Großteil der Arbeit von jemand anderem erledigt wird (und wenn etwas kaputt geht, sind Sie in einer Welt voller Verletzungen und mit Bare-Metal-Dingen geht es oft und dramatisch kaputt).

Zusammenstellen, Kompilieren und Verknüpfen mit Gnu-Werkzeugen, die ich bekomme:

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

Woher weiß der Bootloader also, wo sich das Zeug befindet? Weil der Compiler die Arbeit gemacht hat. Im ersten Fall hat der Assembler den Code für flash.s generiert und weiß dabei, wo sich die Beschriftungen befinden (Beschriftungen sind nur Adressen wie Funktions- oder Variablennamen usw.), sodass ich keine Bytes zählen und den Vektor ausfüllen musste Tabelle manuell, ich habe einen Markennamen verwendet und der Assembler hat es für mich getan. Nun fragt man sich, ob Reset die Adresse 0x14 ist, warum der Assembler 0x15 in die Vektortabelle eingetragen hat. Nun, das ist ein Cortex-m, der bootet und nur im Daumenmodus läuft. Wenn Sie mit ARM zu einer Adresse verzweigen, wenn Sie in den Daumenmodus verzweigen, muss das lsbit gesetzt werden, wenn der Scharfschaltmodus zurückgesetzt wird. Das Bit muss also immer gesetzt sein. Ich kenne die Werkzeuge und setze .thumb_func vor ein Label, wenn dieses Label so wie es ist in der Vektortabelle verwendet wird oder zum Verzweigen nach oder was auch immer. Die Toolchain kann das lsbit setzen. Also hat es hier 0x14 | 1 = 0x15. Ebenso zum Aufhängen. Jetzt zeigt der Disassembler nicht 0x1D für den Aufruf von notmain (), aber keine Sorge, die Tools haben die Anweisung korrekt erstellt.

Nun, da der Code nicht mehr vorhanden ist, werden diese lokalen Variablen nicht verwendet, sondern es handelt sich um toten Code. Der Compiler kommentiert diese Tatsache sogar, indem er sagt, dass y gesetzt, aber nicht verwendet wird.

Beachten Sie den Adressraum, diese Dinge beginnen alle bei der Adresse 0x0000 und gehen von dort aus, damit die Vektortabelle richtig platziert wird, der Text- oder Programmraum auch richtig platziert wird, wie ich flash.s vor notmain.cs Code habe Wenn man die Werkzeuge kennt, ist ein häufiger Fehler, dass man das nicht richtig hinbekommt und abstürzt und hart brennt. IMO müssen Sie zerlegen, um sicherzustellen, dass die Dinge direkt vor dem ersten Start platziert werden. Sobald Sie die Dinge an der richtigen Stelle haben, müssen Sie sie nicht jedes Mal überprüfen. Nur für neue Projekte oder wenn sie hängen.

Nun, einige Leute wundern sich, dass es keinen Grund gibt, zu erwarten, dass zwei Compiler dieselbe Ausgabe von derselben Eingabe erzeugen. Oder sogar derselbe Compiler mit unterschiedlichen Einstellungen. Mit clang, dem llvm-Compiler, bekomme ich diese beiden Ausgaben mit und ohne Optimierung

llvm / clang optimiert

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

nicht optimiert

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

Das ist eine Lüge. Der Compiler hat den Zusatz zwar optimiert, aber zwei Elemente auf dem Stack für die Variablen zugewiesen, da es sich um lokale Variablen handelt, die sich im RAM befinden, aber auf dem Stack nicht an festen Adressen Änderungen. Der Compiler erkannte jedoch, dass es möglich war, y zur Kompilierungszeit zu berechnen, und es gab keinen Grund, es zur Laufzeit zu berechnen. Daher platzierte er einfach eine 1 im für x zugewiesenen Stapelspeicher und eine 2 für den für y zugewiesenen Stapelspeicher. Der Compiler "reserviert" diesen Platz mit internen Tabellen. Ich deklariere Stack plus 0 für Variable y und Stack plus 4 für Variable x. Der Compiler kann tun, was er will, solange der implementierte Code dem C-Standard oder den Erklärungen eines C-Programmierers entspricht. Es gibt keinen Grund, warum der Compiler x für die Dauer der Funktion auf Stack + 4 belassen muss.

Wenn ich einen Funktionsdummy in Assembler hinzufüge

.thumb_func
.globl dummy
dummy:
    bx lr

und dann nenne es

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

die Ausgabe ändert sich

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

Jetzt, da wir verschachtelte Funktionen haben, muss die notmain-Funktion ihre Rücksprungadresse beibehalten, damit sie die Rücksprungadresse für den verschachtelten Aufruf blockieren kann. Dies liegt daran, dass der Arm ein Register für Rückgaben verwendet, wenn er den Stapel wie z. B. ein x86 oder einige andere gut verwendet hat. Er würde den Stapel immer noch verwenden, aber anders. Jetzt fragst du, warum es r4 gedrückt hat? Nun, die Aufrufkonvention wurde vor kurzem geändert, um den Stapel an 64-Bit-Grenzen (zwei Wörter) anstatt an 32-Bit-Grenzen (ein Wort) auszurichten. Sie müssen also etwas verschieben, um den Stapel ausgerichtet zu halten, sodass der Compiler aus irgendeinem Grund willkürlich r4 auswählte, egal warum. Das Aufrufen von r4 wäre ein Fehler, obwohl wir gemäß der Aufrufkonvention für dieses Ziel bei einem Funktionsaufruf nicht r4 blockieren, sondern r0 bis r3 blockieren können. r0 ist der Rückgabewert. Sieht aus wie es vielleicht eine Schwanzoptimierung tut,

Wir sehen jedoch, dass die x- und y-Mathematik auf einen fest codierten Wert von 2 optimiert ist, der an die Dummy-Funktion übergeben wird (Dummy wurde speziell in einer separaten Datei codiert, in diesem Fall asm, damit der Compiler den Funktionsaufruf nicht vollständig optimiert. Wenn ich eine Dummy-Funktion hätte, die einfach in notmain.c in C zurückgegeben wird, hätte der Optimierer den x-, y- und Dummy-Funktionsaufruf entfernt, da sie alle tot / nutzloser Code sind.

Beachten Sie auch, dass Flash.s-Code nicht mehr wichtig ist und die Toolchain sich darum gekümmert hat, alle Adressen für uns zu patchen, sodass wir dies nicht manuell tun müssen.

nicht optimiertes Klirren als Referenz

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}

optimiertes Klirren

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}

Dieser Compiler-Autor hat r7 als Dummy-Variable zum Ausrichten des Stapels ausgewählt. Außerdem erstellt er mit r7 einen Frame-Zeiger, obwohl sich nichts im Stack-Frame befindet. Grundsätzlich hätte der Unterricht optimiert werden können. Aber es wurde der Pop verwendet, um nicht drei Anweisungen zurückzugeben. Das lag wahrscheinlich an mir. Ich wette, ich könnte gcc dazu bringen, dies mit den richtigen Befehlszeilenoptionen (unter Angabe des Prozessors) auszuführen.

Dies sollte hauptsächlich die restlichen Fragen beantworten

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

Ich habe jetzt Globals. Also gehen sie entweder in .data oder .bss, wenn sie nicht optimiert werden.

Bevor wir uns die endgültige Ausgabe ansehen, wollen wir uns das itermediate-Objekt ansehen

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

Jetzt fehlen Informationen, aber es gibt eine Vorstellung davon, was vor sich geht. Der Linker ist derjenige, der Objekte aufnimmt und sie mit den bereitgestellten Informationen verknüpft (in diesem Fall flash.ld), die angeben, wo .text und. Daten und so geht. Der Compiler kennt solche Dinge nicht, er kann sich nur auf den Code konzentrieren, den er präsentiert, und jeder Externe muss eine Lücke hinterlassen, damit der Linker die Verbindung ausfüllt. Alle Daten müssen einen Weg lassen, um diese Dinge miteinander zu verknüpfen, daher basieren die Adressen für alles hier auf Null, nur weil der Compiler und dieser Dissassembler es nicht wissen. Es werden hier weitere Informationen angezeigt, die der Linker zum Platzieren von Dingen verwendet. Der Code hier ist positionsunabhängig genug, damit der Linker seine Arbeit erledigen kann.

Wir sehen dann zumindest eine Zerlegung der verknüpften Ausgabe

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

Der Compiler hat grundsätzlich nach zwei 32-Bit-Variablen im RAM gefragt. Eins ist in .bss, weil ich es nicht initialisiert habe, also wird angenommen, dass es als Null initialisiert wird. Das andere ist .data, weil ich es bei der Deklaration initialisiert habe.

Da es sich um globale Variablen handelt, wird davon ausgegangen, dass andere Funktionen sie ändern können. Der Compiler geht nicht davon aus, wann notmain aufgerufen werden kann, sodass er nicht mit dem optimieren kann, was er sehen kann, nämlich y = x + 1 math. Daher muss er diese Laufzeit ausführen. Es muss aus dem RAM gelesen werden, die beiden Variablen hinzufügen und speichern.

Nun klar, dieser Code wird nicht funktionieren. Warum? weil mein Bootstrap, wie hier gezeigt, den RAM nicht vorbereitet, bevor er notmain aufruft, also wird für y und x verwendet, welcher Müll auch immer in 0x20000000 und 0x20000004 war, als der Chip aufwachte.

Das werde ich hier nicht zeigen. Sie können mein noch längeres Hin und Her auf .data und .bss lesen und warum ich sie nie in meinem Bare-Metal-Code brauche, aber wenn Sie das Gefühl haben, dass Sie die Werkzeuge beherrschen müssen und wollen, anstatt zu hoffen, dass jemand anderes es richtig gemacht hat. .

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

Linker-Skripte und Bootstraps sind etwas compilerspezifisch, sodass alles, was Sie über eine Version eines Compilers erfahren, in die nächste Version oder in einen anderen Compiler verschoben werden kann. Dies ist ein weiterer Grund, warum ich nicht viel Aufwand in die Vorbereitung von .data und .bss stecke nur um so faul zu sein:

unsigned int x=1;

Ich würde das viel lieber tun

unsigned int x;
...
x = 1;

und lassen Sie den Compiler es für mich in .text setzen. Manchmal spart es Blitz, manchmal brennt es mehr. Es ist definitiv viel einfacher, von einer Toolchain-Version oder einem Compiler auf einen anderen zu programmieren und zu portieren. Viel zuverlässiger, weniger fehleranfällig. Ja, entspricht nicht dem C-Standard.

Was ist nun, wenn wir diese statischen Globalen erstellen?

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

Gut

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

Offensichtlich können diese Variablen nicht durch anderen Code geändert werden, sodass der Compiler jetzt zur Kompilierungszeit den toten Code wie zuvor optimieren kann.

nicht optimiert

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

Dieser Compiler, der den Stack für Locals verwendet hat, verwendet jetzt RAM für Globals und dieser Code ist wie geschrieben kaputt, da ich weder mit .data noch .bss richtig umgegangen bin.

und eine letzte Sache, die wir in der Demontage nicht sehen können.

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

Ich änderte x, um mit 0x12345678 pre-init zu sein. Mein Linker-Skript (das ist für Gnu ld) hat dieses Problem. Das teilt dem Linker mit, dass ich möchte, dass der letzte Ort im Adressraum von Ted liegt, aber speichere ihn in der Binärdatei im Adressraum von Ted und jemand wird ihn für Sie verschieben. Und wir können sehen, dass das passiert ist. Dies ist das Intel Hex-Format. und wir können die 0x12345678 sehen

:0400480078563412A0

befindet sich im Flash-Adressraum der Binärdatei.

readelf zeigt das auch

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

Die LOAD-Zeile, in der die virtuelle Adresse 0x20000004 und die physische Adresse 0x48 ist

Oldtimer
quelle
Ganz am Anfang habe ich zwei Unschärfen Bild der Dinge:
user16307
1.) "Der primäre Anwendungsfall besteht darin, Anweisungen in ROM / Flash und Daten in RAM zu haben." Wenn Sie "Daten im RAM hier" sagen, meinen Sie die Daten, die beim Programmablauf entstanden sind. Oder fügen Sie auch die initialisierten Daten ein? Ich meine, wenn wir den Code in das ROM hochladen, sind bereits initialisierte Daten in unserem Code. Zum Beispiel in unserem oode, wenn wir haben: int x = 1; int y = x +1; Im obigen Code gibt es Anweisungen und es gibt Anfangsdaten, die 1 sind. (x = 1). Werden diese Daten auch ins RAM kopiert oder bleiben sie nur im ROM.
user16307
12
hah, ich kenne jetzt die Zeichenbegrenzung für eine Stapelaustauschantwort!
old_timer
2
Du solltest ein Buch schreiben, in dem solche Konzepte Neulingen erklärt werden. "Ich habe zig Beispiele bei Github" - Ist es möglich, ein paar Beispiele zu teilen
AlphaGoku
1
Ich habe es gerade getan. Keiner, der irgendetwas Nützliches tut, aber es ist dennoch ein Beispiel für Code für einen Mikrocontroller. Und ich habe einen Github-Link eingefügt, über den Sie alles andere finden können, was ich geteilt habe, gut, schlecht oder auf andere Weise.
old_timer
8

Diese Antwort wird sich mehr auf den Startprozess konzentrieren. Zuerst wird eine Korrektur durchgeführt - das Schreiben in den Flash erfolgt, nachdem die MCU (oder zumindest ein Teil davon) bereits gestartet wurde. Auf einigen MCUs (normalerweise den fortgeschritteneren) kann die CPU selbst die seriellen Ports bedienen und in die Flash-Register schreiben. Das Schreiben und Ausführen des Programms sind also unterschiedliche Prozesse. Ich gehe mal davon aus, dass das Programm bereits auf Flash geschrieben wurde.

Hier ist der grundlegende Startvorgang. Ich werde einige gebräuchliche Variationen nennen, aber meistens halte ich das einfach.

  1. Zurücksetzen: Es gibt zwei Grundtypen. Das erste ist ein Power-On-Reset, der intern erzeugt wird, während die Versorgungsspannungen ansteigen. Die zweite ist eine externe Stiftumschaltung. Unabhängig davon zwingt das Rücksetzen alle Flip-Flops in der MCU in einen vorbestimmten Zustand.

  2. Zusätzliche Hardware-Initialisierung: Möglicherweise sind zusätzliche Zeit- und / oder Taktzyklen erforderlich, bevor die CPU startet. In den TI-MCUs, an denen ich arbeite, wird beispielsweise eine interne Konfigurations-Scan-Kette geladen.

  3. CPU-Start: Die CPU ruft ihren ersten Befehl von einer speziellen Adresse ab, die als Rücksetzvektor bezeichnet wird. Diese Adresse wird beim Entwurf der CPU festgelegt. Von dort ist es nur normale Programmausführung.

    Die CPU wiederholt immer wieder drei grundlegende Schritte:

    • Abrufen: Lesen Sie einen Befehl (8-, 16- oder 32-Bit-Wert) von der im Programmzählerregister (PC) gespeicherten Adresse und erhöhen Sie dann den PC.
    • Dekodieren: Wandelt die Binäranweisung in einen Wertesatz für die internen Steuersignale der CPU um.
    • Execute: Führen Sie den Befehl aus - fügen Sie zwei Register hinzu, lesen oder schreiben Sie in den Speicher, verzweigen Sie (wechseln Sie den PC) oder was auch immer.

    (Tatsächlich ist es komplizierter. CPUs sind normalerweise per Pipeline verbunden , was bedeutet, dass sie jeden der oben genannten Schritte gleichzeitig mit verschiedenen Befehlen ausführen können. Jeder der oben genannten Schritte kann mehrere Pipelinestufen haben. Dann gibt es parallele Pipelines, Verzweigungsvorhersage und all das ausgefallene Zeug zur Computerarchitektur, das diese Intel-CPUs dazu bringt, eine Milliarde Transistoren für das Design zu benötigen.)

    Sie fragen sich vielleicht, wie der Abruf funktioniert. Die CPU verfügt über einen Bus , der aus Adressensignalen (out) und Datensignalen (in / out) besteht. Um einen Abruf durchzuführen, setzt die CPU ihre Adressleitungen auf den Wert im Programmzähler und sendet dann einen Takt über den Bus. Die Adresse wird dekodiert, um einen Speicher freizugeben. Der Speicher empfängt die Uhr und die Adresse und setzt den Wert an dieser Adresse auf die Datenleitungen. Die CPU erhält diesen Wert. Das Lesen und Schreiben von Daten ist ähnlich, mit der Ausnahme, dass die Adresse aus dem Befehl oder einem Wert in einem Universalregister stammt und nicht vom PC.

    CPUs mit einer von Neumann-Architektur haben einen einzigen Bus, der sowohl für Anweisungen als auch für Daten verwendet wird. CPUs mit einer Harvard-Architektur haben einen Bus für Anweisungen und einen für Daten. In echten MCUs können beide Busse mit denselben Speichern verbunden sein, sodass Sie sich häufig (aber nicht immer) keine Sorgen machen müssen.

    Zurück zum Startvorgang. Nach dem Zurücksetzen wird der PC mit einem Startwert geladen, der als Rücksetzvektor bezeichnet wird. Dies kann in die Hardware eingebaut oder (bei ARM Cortex-M-CPUs) automatisch aus dem Speicher ausgelesen werden. Die CPU holt den Befehl aus dem Rücksetzvektor und beginnt, die obigen Schritte zu durchlaufen. Zu diesem Zeitpunkt arbeitet die CPU normal.

  4. Bootloader: Häufig müssen einige Einstellungen auf niedriger Ebene vorgenommen werden, um den Rest der MCU betriebsbereit zu machen. Dies kann z. B. das Löschen von RAMs und das Laden von Einstellungen für den Fertigungsabgleich für analoge Komponenten umfassen. Möglicherweise besteht auch die Möglichkeit, Code von einer externen Quelle wie einem seriellen Anschluss oder einem externen Speicher zu laden. Die MCU kann ein Boot-ROM enthalten , das ein kleines Programm enthält, um diese Dinge zu tun. In diesem Fall zeigt der CPU-Rücksetzvektor auf den Adressraum des Boot-ROM. Dies ist im Grunde ein normaler Code, der nur vom Hersteller bereitgestellt wird, sodass Sie ihn nicht selbst schreiben müssen. :-) In einem PC entspricht das BIOS dem Boot-ROM.

  5. C-Umgebungssetup: C erwartet einen Stack (RAM-Bereich zum Speichern des Status während Funktionsaufrufen) und initialisierte Speicherorte für globale Variablen. Dies sind die Abschnitte .stack, .data und .bss, über die Dwelch spricht. Bei initialisierten globalen Variablen werden die Initialisierungswerte in diesem Schritt vom Flash in den RAM kopiert. Nicht initialisierte globale Variablen haben nahe beieinander liegende RAM-Adressen, sodass der gesamte Speicherblock sehr einfach auf Null initialisiert werden kann. Der Stapel muss nicht initialisiert werden (obwohl dies möglich ist). Sie müssen lediglich das Stapelzeigerregister der CPU so einstellen, dass es auf einen zugewiesenen Bereich im RAM zeigt.

  6. Hauptfunktion : Sobald die C-Umgebung eingerichtet ist, ruft der C-Loader die main () -Funktion auf. Hier beginnt normalerweise Ihr Anwendungscode. Wenn Sie möchten, können Sie die Standardbibliothek auslassen, das Setup der C-Umgebung überspringen und Ihren eigenen Code schreiben, um main () aufzurufen. Bei einigen MCUs können Sie möglicherweise Ihren eigenen Bootloader schreiben und dann das gesamte Setup auf niedriger Ebene selbst durchführen.

Verschiedenes: Bei vielen MCUs können Sie Code aus dem RAM ausführen, um die Leistung zu verbessern. Dies wird normalerweise in der Linkerkonfiguration festgelegt. Der Linker weist jeder Funktion zwei Adressen zu - eine Ladeadresse , an der der Code zuerst gespeichert wird (normalerweise Flash), und eine Laufadresse , die auf den PC geladen wird, um die Funktion auszuführen (Flash oder RAM). Um Code aus dem RAM auszuführen, schreiben Sie Code, damit die CPU den Funktionscode von seiner Ladeadresse im Flash in ihre Laufadresse im RAM kopiert und dann die Funktion an der Laufadresse aufruft. Der Linker kann dazu globale Variablen definieren. Das Ausführen von Code aus dem RAM ist in MCUs jedoch optional. Normalerweise tun Sie dies nur, wenn Sie wirklich hohe Leistung benötigen oder den Flash neu schreiben möchten.

Adam Haun
quelle
1

Ihre Zusammenfassung entspricht in etwa der von Neumann-Architektur . Der anfängliche Code wird normalerweise über einen Bootloader in den RAM geladen, jedoch nicht (normalerweise) über einen Software-Bootloader, auf den sich der Begriff bezieht. Dies ist normalerweise ein in das Silizium eingebranntes Verhalten. Die Codeausführung in dieser Architektur beinhaltet häufig ein vorausschauendes Zwischenspeichern von Befehlen aus dem ROM, so dass der Prozessor die Zeit maximiert, in der der Code ausgeführt wird, und nicht darauf wartet, dass der Code in den RAM geladen wird. Ich habe irgendwo gelesen, dass der MSP430 ein Beispiel für diese Architektur ist.

In einer Harvard-Architektur Gerät von werden Anweisungen direkt aus dem ROM ausgeführt, während auf den Datenspeicher (RAM) über einen separaten Bus zugegriffen wird. In dieser Architektur wird der Code einfach vom Rücksetzvektor aus ausgeführt. Der PIC24 und der dsPIC33 sind Beispiele für diese Architektur.

Das tatsächliche Umkehren von Bits, die diese Prozesse auslösen, kann von Gerät zu Gerät variieren und Debugger, JTAG, proprietäre Methoden usw. umfassen.

leicht geknickt
quelle
Aber Sie überspringen schnell einige Punkte. Nehmen wir es in Zeitlupe. Nehmen wir an, der Binärcode "first" wird in das ROM geschrieben. Ok .. Danach schreibst du "Datenspeicher wird angesprochen" .... Aber woher kommen die Daten "ins RAM" zuerst beim Hochfahren? Kommt es wieder aus ROM? Und wenn ja, woher weiß der Bootloader, welcher Teil des ROM zu Beginn in den RAM geschrieben wird?
user16307
Sie haben Recht, ich habe viel übersprungen. Die anderen Jungs haben bessere Antworten. Ich bin froh, dass du das hast, wonach du gesucht hast.
leicht