Codeausführung in eingebetteten Systemen

8

Ich arbeite in der eingebetteten Systemdomäne. Ich würde gerne wissen, wie ein Code von einem Mikrocontroller ausgeführt wird (uC muss im Allgemeinen nicht subjektiv sein), ausgehend von einer C-Datei. Auch würde ich gerne Sachen wie Startup-Code, Objektdatei usw. wissen. Ich konnte keine Online-Dokumentationen über die oben genannten Sachen finden. Wenn möglich, geben Sie bitte Links an, die diese Dinge von Grund auf erklären. Vielen Dank im Voraus für Ihre Hilfe

    
inquisitive 02.09.2009, 10:46
quelle

7 Antworten

36

Als Mikroprozessorarchitekt hatte ich die Möglichkeit, auf sehr niedrigem Niveau für Software zu arbeiten. Grundsätzlich unterscheidet sich Low-Level-Embedded von der allgemeinen PC-Programmierung nur auf der Hardware-spezifischen Ebene.

Low-Level Embedded Software kann wie folgt unterteilt werden:

  1. Vektor zurücksetzen - dies wird normalerweise in Assembly geschrieben. Es ist die allererste Sache, die beim Start ausgeführt wird und als hardwarespezifischer Code betrachtet werden kann. Es führt normalerweise einfache Funktionen wie das Einrichten des Prozessors in einen vordefinierten stabilen Zustand durch Konfigurieren von Registern und dergleichen aus. Dann springt es zum Startup-Code. Der grundlegendste Rücksetzvektor springt lediglich direkt zum Startcode.
  2. Startcode - Dies ist der erste softwarespezifische Code, der ausgeführt wird. Seine Aufgabe besteht im Wesentlichen darin, die Softwareumgebung so einzurichten, dass C-Code oben ausgeführt werden kann. Zum Beispiel geht C-Code davon aus, dass es einen Speicherbereich gibt, der als Stack und Heap definiert ist. Dies sind normalerweise Softwarekonstrukte anstelle von Hardware. Daher wird dieser Startcode die Stapelzeiger und Heap-Zeiger und dergleichen definieren. Dies wird normalerweise unter der c-Laufzeitumgebung gruppiert. Für C ++ - Code werden auch Konstruktoren aufgerufen. Am Ende der Routine wird main() ausgeführt. edit: Variablen, die initialisiert werden müssen, sowie bestimmte Teile des Speichers, die gelöscht werden müssen, werden hier ausgeführt. Im Grunde alles, was nötig ist, um Dinge in einen "bekannten Zustand" zu versetzen.
  3. Anwendungscode - dies ist Ihre eigentliche C-Anwendung, beginnend mit der Funktion main() . Wie du sehen kannst, sind viele Dinge tatsächlich unter der Haube und passieren sogar bevor deine erste Hauptfunktion aufgerufen wird. Dieser Code kann normalerweise als Hardware-unabhängig geschrieben werden, wenn eine gute Hardware-Abstraktionsschicht verfügbar ist. Der Anwendungscode wird definitiv viele Bibliotheksfunktionen nutzen. Diese Bibliotheken sind normalerweise in eingebetteten Systemen statisch verknüpft.
  4. Bibliotheken - Dies sind Ihre Standard-C-Bibliotheken, die primitive C-Funktionen bereitstellen. Es gibt auch prozessorspezifische Bibliotheken, die Dinge wie die Fließkomma-Unterstützung von Software implementieren. Es können auch hardwarespezifische Bibliotheken für den Zugriff auf die E / A-Geräte und für stdin / stdout vorhanden sein. Ein paar gebräuchliche C-Bibliotheken sind Newlib und uClibc .
  5. Unterbrechen / Ausnahme Handler - dies sind Routinen, die während der normalen Codeausführung als Ergebnis von Hardware- oder Prozessorstatusänderungen zu zufälligen Zeiten ausgeführt werden. Diese Routinen werden in der Regel auch in Assembly geschrieben, da sie mit minimalem Software-Overhead ausgeführt werden sollten, um die eigentliche Hardware namens.
  6. zu bedienen

Ich hoffe, das wird einen guten Start ermöglichen. Fühlen Sie sich frei, Kommentare zu hinterlassen, wenn Sie andere Fragen haben.

    
sybreon 02.09.2009, 11:24
quelle
5

Im Allgemeinen arbeiten Sie viel niedriger als allgemeine Computer.

Jede CPU wird beim Einschalten ein bestimmtes Verhalten haben, wie das Löschen aller Register und das Setzen des Programmzählers auf 0xf000 (alles hier ist nicht spezifisch, wie es Ihre Frage ist).

Der Trick besteht darin, sicherzustellen, dass Ihr Code am richtigen Ort ist.

Der Kompilierungsprozess ähnelt normalerweise allgemeinen Computern, indem Sie C in Maschinencode (Objektdateien) übersetzen. Von dort müssen Sie diesen Code mit verknüpfen:

  • Ihr System Start-up-Code, oft in Assembler.
  • alle Laufzeitbibliotheken (einschließlich der erforderlichen Bits der C RTL).

Der Systemstart-Code initialisiert im Allgemeinen nur die Hardware und richtet die Umgebung so ein, dass Ihr C-Code funktionieren kann. Runtime-Bibliotheken in Embedded-Systemen machen oft die großen sperrigen Sachen (wie Fließkomma-Unterstützung oder printf) optional, um Code-Bloat niedrig zu halten.

Der Linker in eingebetteten Systemen ist normalerweise auch viel einfacher und gibt anstelle von verschiebbaren Binärdateien Code mit festem Standort aus. Sie verwenden es, um sicherzustellen, dass der Startcode bei (z. B.) 0xf000 geht.

In eingebetteten Systemen möchten Sie normalerweise, dass der ausführbare Code von Anfang an vorhanden ist, damit Sie ihn in ein EPROM (oder EEPROM oder Flash oder ein anderes Gerät, das Inhalte beim Ausschalten beibehält) brennen können.

Denken Sie natürlich daran, dass mein letzter Ausflug mit 8051 und 68302 Prozessoren war. Es kann sein, dass "eingebettete" Systeme heute vollgepackte Linux-Boxen mit allerlei wundervoller Hardware sind. In diesem Fall gäbe es keinen wirklichen Unterschied zwischen Allzweck und Embedded.

Aber ich bezweifle es. Es besteht immer noch Bedarf an einer Hardware mit sehr niedriger Spezifikation, die benutzerdefinierte Betriebssysteme und / oder Anwendungscode benötigt.

SPJ Embedded Technologies hat ein herunterladbares Evaluierung ihrer 8051-Entwicklungsumgebung, die so aussieht, wie Sie es sich wünschen. Sie können Programme mit einer Größe von bis zu 2K erstellen, aber es scheint den gesamten Prozess zu durchlaufen (Kompilieren, Erzeugen von HEX- oder BIN-Dateien für das Dumping auf die Zielhardware, sogar einen Simulator, der Zugriff auf das On-Chip-Zeug und externe Geräte ermöglicht) ).

Das Nicht-Bewertungsprodukt kostet 200 Euro, aber wenn alles, was Sie wollen, ein bisschen ein Spiel ist, würde ich einfach die Bewertung herunterladen - anders als das 2K-Limit, es ist das vollständige Produkt.

    
paxdiablo 02.09.2009 10:52
quelle
2

Ich habe den Eindruck, dass Sie am meisten daran interessiert sind, was sybreon "Schritt 2" nennt. Viele können dort passieren, und es variiert stark nach Plattform. In der Regel wird dieses Zeug von einer Kombination aus Bootloader, Board-Support-Paket, C Runtime (CRT) und, wenn Sie eine haben, das Betriebssystem.

behandelt

Normalerweise wird nach dem Reset-Vektor eine Art Bootloader vom Flash ausgeführt. Dieser Bootloader kann nur Hardware einrichten und in den CRT Ihrer App springen, auch im Flash. In diesem Fall würde die CRT wahrscheinlich die .bss löschen, die .data in den RAM kopieren usw. In anderen Systemen kann der Bootloader die App aus einer codierten Datei, wie einer ELF, streuen, und die CRT richtet einfach andere ein Laufzeit-Zeug (Heap usw.). All dies geschieht, bevor der CRT die main (). Der App aufruft.

Wenn Ihre App statisch verknüpft ist, geben Linker-Direktiven die Adressen an, unter denen .data / .bss und stack initialisiert werden. Diese Werte sind entweder mit der CRT verknüpft oder in der ELF codiert. In einer dynamisch verknüpften Umgebung wird das Laden von Anwendungen normalerweise von einem Betriebssystem gehandhabt, das die ELF neu anlegt, um in welchem ​​Speicher das OS zu betreiben.

Außerdem führen einige Ziele Apps aus dem Flash aus, andere kopieren den ausführbaren Text von Flash in den Arbeitsspeicher. (Dies ist normalerweise ein Kompromiss aus Geschwindigkeit und Footprint, da RAM bei den meisten Zielen schneller / breiter ist als Flash.)

    
Casey Barker 02.09.2009 16:56
quelle
2

Ok, ich gebe das eine Chance ...

Erste Architekturen. Von Neumann gegen Harvard. Harvard-Architektur verfügt über separaten Speicher für Code und Daten. Von Neumann nicht. Harvard wird in vielen Mikrocontrollern verwendet, und das ist mir vertraut.

Also beginnend mit Ihrer grundlegenden Harvard-Architektur haben Sie Programmspeicher. Wenn der Mikrocontroller das erste Mal startet, führt er die Anweisungen am Speicherplatz Null aus. Normalerweise ist dies ein JUMP-Befehl, mit dem der Hauptcode gestartet wird.

Wenn ich jetzt Anweisungen sage, meine ich Opcodes. Opcodes sind Befehle, die in Binärdaten codiert sind - normalerweise 8 oder 16 Bits. In einigen Architekturen ist jeder Opcode fest codiert, um bestimmte Dinge zu bedeuten, in anderen kann jedes Bit signifikant sein (dh Bit 1 bedeutet Prüfübertrag, Bit 2 bedeutet Prüf-Nullkennzeichen usw.). Also gibt es Opcodes und dann Parameter für die Opcodes. Ein JUMP-Befehl ist ein Opcode und eine 8- oder 16- oder 32-Bit-Speicheradresse, zu der der Code "springt". Dh, die Kontrolle wird auf die Anweisungen an dieser Adresse übertragen. Dies wird erreicht, indem ein spezielles Register manipuliert wird, das die Adresse des nächsten auszuführenden Befehls enthält. Um zu JUMP an Speicherplatz 0x0050 zu springen, würde es den Inhalt dieses Registers durch 0x0050 ersetzen. Beim nächsten Taktzyklus würde der Prozessor das Register lesen und die Speicheradresse lokalisieren und die Anweisung dort ausführen.

Das Ausführen von Anweisungen führt zu Änderungen im Status der Maschine. Es gibt ein allgemeines Statusregister, das Informationen darüber speichert, was der letzte Befehl getan hat (dh, wenn es ein Zusatz ist, dann, wenn ein Ausführen erforderlich war, gibt es ein Bit dafür usw.). Es gibt ein "Akkumulator" -Register, in das das Ergebnis der Anweisung eingegeben wird. Die Parameter für Befehle können entweder in eines von mehreren Allzweckregistern oder im Akkumulator oder in Speicheradressen (Daten ODER Programm) eingegeben werden. Verschiedene Opcodes können nur an Daten an bestimmten Orten ausgeführt werden. Zum Beispiel könnten Sie Daten aus zwei Allzweckregistern ADDIEREN und das Ergebnis im Akkumulator anzeigen lassen, aber Sie können keine Daten von zwei Datenspeicherorten übernehmen und das Ergebnis an einem anderen Datenspeicherort anzeigen. Sie müssten die gewünschten Daten in die allgemeinen Register verschieben, die Addition durchführen und das Ergebnis an den gewünschten Speicherort verschieben. Deshalb gilt die Montage als schwierig. Es gibt so viele Statusregister, wie für die Architektur vorgesehen sind. Komplexere Architekturen können mehr haben, um komplexere Befehle zu ermöglichen. Einfachere können nicht.

Es gibt auch einen Speicherbereich, der als Stack bekannt ist. Es ist nur ein Bereich im Speicher für einige Mikrocontroller (wie der 8051). In anderen kann es spezielle Schutze haben. Es gibt ein Register namens Stack-Pointer, das aufzeichnet, an welcher Speicherstelle sich die 'Oberseite' des Stacks befindet. Wenn Sie etwas vom Akkumulator auf den Stack "pushen", wird die "obere" Speicheradresse inkrementiert und die Daten vom Akkumulator werden in die vorherige Adresse gesetzt. Wenn Daten vom Stapel abgerufen oder ausgegeben werden, wird der umgekehrte Vorgang ausgeführt, und die Stapelzeiger werden dekrementiert, und die Daten vom Stapel werden in den Akkumulator gegeben.

Jetzt habe ich auch irgendwie übergossen, wie Anweisungen "ausgeführt" werden. Nun, das ist, wenn Sie auf digitale Logik - VHDL Art von Sachen. Multiplexer und Decoder und Wahrheitstabellen und dergleichen. Das ist die wahre Kleinigkeit des Designs - irgendwie. Wenn Sie also den Inhalt eines Speicherplatzes in den Akkumulator "verschieben" wollen, müssen Sie die Adressierungslogik herausfinden, das Akkumulatorregister löschen, UND es mit den Daten am Speicherort, usw. zusammensetzen. Es ist entmutigend, wenn alle zusammen gesetzt werden, aber wenn Sie haben separate Teile (wie Adressierung, einen Halbaddierer, usw.) in VHDL oder in irgendeiner digitalen Logikart gemacht, Sie könnten eine Idee haben, was erforderlich ist.

Wie verhält es sich mit C? Nun, ein Compiler wird die C-Anweisungen nehmen und sie in eine Reihe von Opcodes umwandeln, die die angeforderten Operationen ausführen. All das sind grundsätzlich hex Daten - Eins und Nullen, die irgendwann im Programmspeicher abgelegt werden. Dies geschieht mit Compiler- / Linker-Direktiven, die angeben, welcher Speicherort für welchen Code verwendet wird. Es wird in den Flash-Speicher auf dem Chip geschrieben, und dann, wenn der Chip neu startet, geht es zum Code-Speicherort 0x0000 und springt zur Startadresse des Codes im Programmspeicher, dann fängt er an, die Opcodes zu verstopfen.

    
Stephen Friederichs 03.09.2009 01:54
quelle
1

Ich habe Erfahrung mit AVR-Mikrocontrollern, aber ich denke, das wird für alle ziemlich gleich sein:

Die Kompilierung verläuft in der gleichen Weise wie bei einem normalen C-Code. Es wird in die Objektdateien kompiliert, die miteinander verknüpft sind, aber anstatt einige komplexe Formate wie ELF oder PE auszugeben, wird die Ausgabe einfach auf eine feste Adresse im Speicher des UC ohne Header gesetzt.

Der Startcode (wenn der Compiler einen generiert) wird auf die gleiche Weise wie der Startup-Code für "normale" Computer hinzugefügt - vor Ihrem main () - Code (und möglicherweise auch danach) ist Code hinzugefügt.

Ein weiterer Unterschied ist die Verknüpfung - alles muss statisch verknüpft werden, da Mikrocontroller kein Betriebssystem haben, um die dynamische Verknüpfung zu handhaben.

    
cube 02.09.2009 11:00
quelle
1

Sie können sich das sehr detaillierte GNU ARM Tutorial von Jim Lynch ansehen.

    
starblue 02.09.2009 19:49
quelle
0

Sie können auf den Link Ссылка verweisen.

Die folgende Sequenz zeigt die Reihenfolge der Ausführungen der Controller-Anweisungen:

1) Ordnet den primären Speicher für die Ausführung des Programms zu.

2) Kopiert den Adressraum vom sekundären zum primären Speicher.

3) Kopiert die Abschnitte .text und .data von der ausführbaren Datei in den primären Speicher.

4) Kopiert Programmargumente (z. B. Befehlszeilenargumente) auf den Stapel.

5) Initialisiert die Register: Setzt den esp (Stapelzeiger) so, dass er auf den obersten Stapel zeigt, löscht den Rest.

6) Springt zum Starten der Routine, die: main () -Argumente aus dem Stapel kopiert und zu main () springt.

    
Shaikmeera 24.05.2016 18:43
quelle

Tags und Links