Entdecken Sie Millionen von E-Books, Hörbüchern und vieles mehr mit einer kostenlosen Testversion

Nur $11.99/Monat nach der Testphase. Jederzeit kündbar.

Embedded Software Timing: Methodik, Analyse und Praxistipps am Beispiel Automotive
Embedded Software Timing: Methodik, Analyse und Praxistipps am Beispiel Automotive
Embedded Software Timing: Methodik, Analyse und Praxistipps am Beispiel Automotive
eBook708 Seiten5 Stunden

Embedded Software Timing: Methodik, Analyse und Praxistipps am Beispiel Automotive

Bewertung: 0 von 5 Sternen

()

Vorschau lesen

Über dieses E-Book

Die Zahl der Embedded Systeme, die uns im Alltag begegnen, wächst stetig. Gleichzeitig nimmt die Komplexität der Software immer weiter zu. In vielen Bereichen erhält Multicore Einzug, was die Komplexität nochmals erhöht.Ohne korrektes zeitliches Verhalten („Timing“) gibt es keine sichere und zuverlässige Embedded Software. Dieses Buch hilft gleichermaßen das Timing schon früh im Entwicklungsprozess zu berücksichtigen und akute Timingprobleme zu lösen. Auch der Aspekt der Laufzeitabsicherung kommt nicht zu kurz.Auch wenn die meisten Praxisbeispiele aus dem Automobilbereich kommen, ist der allergrößte Teile des Buches unmittelbar übertragbar auf andere Bereiche.
SpracheDeutsch
HerausgeberSpringer Vieweg
Erscheinungsdatum4. Jan. 2021
ISBN9783658264802
Embedded Software Timing: Methodik, Analyse und Praxistipps am Beispiel Automotive

Ähnlich wie Embedded Software Timing

Ähnliche E-Books

Softwareentwicklung & -technik für Sie

Mehr anzeigen

Ähnliche Artikel

Rezensionen für Embedded Software Timing

Bewertung: 0 von 5 Sternen
0 Bewertungen

0 Bewertungen0 Rezensionen

Wie hat es Ihnen gefallen?

Zum Bewerten, tippen

Die Rezension muss mindestens 10 Wörter umfassen

    Buchvorschau

    Embedded Software Timing - Peter Gliwa

    © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021

    P. GliwaEmbedded Software Timinghttps://doi.org/10.1007/978-3-658-26480-2_1

    1. Allgemeine Grundlagen

    Peter Gliwa¹  

    (1)

    Gliwa GmbH, Weilheim, Bayern, Deutschland

    Peter Gliwa

    Email: peter.gliwa@gliwa.com

    Elektronisches Zusatzmaterial

    Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht https://​doi.​org/​10.​1007/​978-3-658-26480-2_​1.

    Grundlegendes Wissen in den Bereichen Softwareentwicklung und Betriebssysteme sind Voraussetzung für Analyse und Optimierung des Timings von Embedded Software. Das Kapitel „Allgemeine Grundlagen verfolgt zwei Ziele. Zum einen sollen wichtige Grundlagen vermittelt bzw. zusammengefasst werden. Zum anderen wird schon an dieser Stelle bei den einzelnen Themen ein Bezug zum Timing hergestellt. Daher richtet sich das Kapitel nicht ausschließlich an diejenigen, die Grundlagen erlernen oder auffrischen möchten. Auch erfahrenen Softwareentwicklern hilft die neue Facette „Timing, die sich bei Altbekanntem zeigt.

    1.1 Echtzeit

    Fragt man Entwickler von Desktopsoftware oder Webanwendungen, was sie unter Echtzeit verstehen, bekommt man mitunter die Antwort, dass Echtzeit im Sinne von „ganz schnell oder mit „mit ganz wenig Verzögerung zu verstehen ist.

    Auch wenn das für die meisten Echtzeitsysteme sicherlich nicht falsch ist, trifft es doch nicht den Kern der Sache. Echtzeit im Umfeld von Embedded Software sollte man eher im Sinne von „rechtzeitig" verstehen. Es existieren zeitliche Anforderungen, sogenannte Timinganforderungen, die eingehalten werden müssen. Bei harter Echtzeit muss die Einhaltung der Timinganforderungen unter allen Umständen gewährleistet sein, bei weicher Echtzeit reicht es aus, wenn die Timinganforderungen nicht zu häufig verletzt werden, wenn sie also statistisch garantiert werden können. Welcher statistischer Parameter zur Erfüllung der weichen Echtzeit herangezogen wird, ist nicht allgemeingültig definiert. Bei Bedarf muss für ein gegebenes Projekt, für eine gegebene Situation eine eigene Definition gefunden werden.

    1.2 Phasengetriebenes Prozessmodell: das V-Modell

    Das V-Modell beschreibt ein Konzept zur Vorgehensweise bei der Softwareentwicklung. Es kommt seit Jahrzehnten  im Automobilbereich zum Einsatz und ist meist auch dann – zumindest im Hintergrund  – vorhanden, wenn nach neueren Konzepten wie Scrum entwickelt wird. Seinen Ursprung hat es, wie so viele technische Entwicklungen, im militärischen Sektor. Später wurde es auf den zivilen Bereich übertragen und in den Ausprägungen V-Modell 97 und V-Modell XT an die neueren Anforderungen bei der Entwicklung angepasst [1].

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Fig1_HTML.png

    Abb. 1.1

    V-Modell der Softwareentwicklung

    Das „V des V-Modells stellt den idealisierten Verlauf der Entwicklung in einem Koordinatensystem mit zwei Achsen dar. Die horizontale Achse ist eine Zeitachse beginnend links mit dem Start des Projektes. Die vertikale Achse markiert die Abstraktion: von „detailliert unten bis „abstrahiert" oben. Siehe auch Abb. 1.1. Ein Projekt sollte auf einer hohen Abstraktionsebene mit dem Einsammeln der Nutzer- oder Kundenanforderungen (Englisch: „Requirements") an das Produkt starten. Es folgt auf Systemebene das grundsätzliche Design des Produktes. Im weiteren Verlauf des Projektes wird das Design heruntergebrochen, verfeinert, detailliert. Eventuell ergeben sich auch weitere, detailliertere Anforderungen an das Produkt. Ist die Designphase abgeschlossen, startet die Implementierung, also die Umsetzung. Bezogen auf ein Softwareprojekt entspricht dies der Codierung. Dem Zusammenfügen der einzelnen Komponenten, der Integration, folgt die Absicherung, die Verifikation auf den verschiedenen Abstraktionsebenen. Dabei werden die zuvor formulierten Anforderungen der jeweiligen Ebene überprüft. Die letzte Überprüfung erfolgt auf der obersten Abstraktionsebene, indem sichergestellt wird, dass die Nutzer- oder Kundenanforderungen erfüllt werden.

    Wird eine Anforderung nicht erfüllt, muss die Ursache der Abweichung beseitigt werden. Die Ursache liegt zwangsläufig irgendwo auf dem V zwischen der Anforderung und deren Überprüfung. In der Folge müssen alle abhängigen nachfolgenden Schritte ebenfalls korrigiert, angepasst oder zumindest wiederholt werden.

    Es liegt auf der Hand, dass der von Fehlern verursachte Aufwand und auch die Kosten umso größer werden, je später die Fehler entdeckt werden. Das liest sich wie eine Binsenweisheit, doch ist es erstaunlich, wie viele Projekte das Embedded Software Timing stiefmütterlich behandeln. Viel zu oft werden in einer späten Projektphase mit viel Hektik, hohen Kosten und und großem Risiko Laufzeitprobleme untersucht und notdürftig behoben oder abgemildert.

    1.2.1 Das V-Modell im Zusammenhang mit Timing

    Praktisch jeder Softwareentwickler im Automobilbereich kennt das V-Modell wie in Abschn. 1.2 dargestellt. Bei der Verwendung des V-Modells stehen meist die funktionalen Aspekte im Mittelpunkt. Wie sieht es nun aus, wenn das Thema Timing ins Spiel kommt? Im Prinzip ändert sich nichts. Die dem Modell zugrunde liegende Idee kann auch auf das Timing angewendet werden. Abb. 1.2 zeigt eine Konkretisierung der Idee und gibt aufs Timing bezogene Beispiele für die unterschiedlichen Phasen des V-Modells.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Fig2_HTML.png

    Abb. 1.2

    V-Modell angewandt auf Timing-bezogene Aktivitäten

    Kap. 9 beschäftigt sich eingehend damit, wie Timinganalyse systematisch in den Entwicklungsprozess integriert werden kann.

    1.3 Buildprozess: vom Modell zum Executable

    Zwischen dem linken Ast des V-Modells und dem Prozess , der aus dem Quellcode ausführbaren Maschinencode werden lässt, dem Buildprozess, gibt es Analogien. Gestartet wird auf einer vergleichsweisen hohen Abstraktionsebene und im Verlauf der Zeit nähert man sich immer mehr der ausführenden Hardware, dem Prozessor an.

    Dieser Abschnitt beschreibt, wie aus dem Quellcode ausführbarer Maschinencode – also ein Executable – wird und welche Dateien, Werkzeuge und Übersetzungsschritte dabei relevant sind. Die in dem Abschnitt behandelten Grundlagen haben „nur" einen indirekten Bezug zum Thema Timing. Doch ohne das Verständnis, wie zum Beispiel ein Compiler grundsätzlich funktioniert, ist Codeoptimierung mit dem Ziel der Laufzeitminimierung nur schwer möglich.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Fig3_HTML.png

    Abb. 1.3

    Der Buildprozess: Werkzeuge und Dateien auf dem Weg zum Executable

    1.3.1 Modellbasierte Softwareentwicklung, Codegenerierung

    Mittlerweile ist wohl der größere  Anteil der Software, die in einem Auto läuft, modellbasiert. Das heißt, dass der Quellcode nicht von Hand geschrieben ist, sondern von Code generierenden Werkzeugen wie Embedded Coder, Targetlink oder ASCET erzeugt wird. Zuvor wurde die Funktionalität – meist Regelungstechnik, digitale Filter oder Zustandsautomaten – mit grafischen Modellierungswerkzeugen wie MATLAB/Simulink oder ASCET formuliert und als „Modell" gespeichert.

    1.3.2 C Präprozessor

    Listing 1.1 zeigt ein einfaches – in diesem Fall von Hand codiertes – Programm. Anhand dieses Programms soll im Folgenden der Weg vom Quellcode zum ausführbaren Programm, zum Executable, veranschaulicht werden. Der Code des eingebundenen Headers myTypes.h ist in Listing 1.2 zu sehen.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Figa_HTML.png../images/478274_1_De_1_Chapter/478274_1_De_1_Figb_HTML.png

    Das in Zeile 12 Listing 1.1 verwendete Schlüsselwort ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq1_HTML.gif (das englische Wort „volatile bedeutet „flüchtig) veranlasst den Compiler dazu, jeden Zugriff auf die betroffene Variable explizit im Speicher vorzunehmen und nicht etwa in einem Register zwischenzuspeichern. Das ist beispielsweise dann erforderlich, wenn der Inhalt der betroffenen Speicherstelle von der Hardwareperipherie beschrieben werden kann. Das Timerregister eines Hardwaretimers ist ein Beispiel dafür. In Listing 1.1 wird ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq2_HTML.gif verwendet, um zu verhindern, dass der Compiler den Code „wegoptimiert, also feststellt, dass die Variable ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq3_HTML.gif nie „sinnvoll verwendet wird und daher sämtliche Zugriffe darauf entfernt.

    Abb. 1.3 zeigt, welche Schritte auf dem Weg vom Quellcode zum Executable durchlaufen werden, welche Zwischenformate anfallen und welche zusätzlichen Dateien involviert sind. Optionale Datenflüsse, Dateien und Werkzeuge sind blass dargestellt.

    Im ersten Schritt analysiert der Präprozessor des Compilers den Code und löst alle Makros („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq4_HTML.gif ) auf, liest alle eingebundenen Dateien ein („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq5_HTML.gif ), entfernt inaktiven Code bedingter Kompilierung („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq6_HTML.gif (...) ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq7_HTML.gif ) und berechnet alle Werte, die zu diesem Zeitpunkt bereits berechenbar sind („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq8_HTML.gif $$\rightarrow $$ „ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq10_HTML.gif ). Alle Anweisungen, die mit einem „ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq11_HTML.gif beginnen, sind Präprozessoranweisungen. Tatsächlich erledigt der Präprozessor noch eine ganze Reihe weiterer Aufgaben, doch sollen die genannten Beispiele an dieser Stelle reichen, um das Prinzip zu verdeutlichen.

    Tipp

    Die meisten Compiler unterstützen die Kommandozeilenoption ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq12_HTML.gif , die den Compiler dazu veranlasst, nach der Präprozessorstufe abzubrechen und den „präprozessierten" Code auf ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq13_HTML.gif auszugeben. Das kann sehr hilfreich beim Debuggen von Problemen im Zusammenhang mit dem Präprozessor sein.

    Außerdem eignet sich diese Ausgabe auch sehr gut, um Compilerprobleme an den Compilerhersteller zu melden. Wird die Ausgabe in eine Datei umgeleitet (hier hat sich die Dateiendung ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq14_HTML.gif eingebürgert), kann diese Datei ohne weitere Dateien – wie beispielsweise inkludierten Headern – dem Compiler zum kompilieren übergeben werden. Der Compilerhersteller kann dann das Problem nachvollziehen, ohne dass er dafür Zugriff auf alle eingebundenen Header benötigt.

    Listing 1.3 zeigt die in eine Datei main.i umgeleitete Präprozessorausgabe für die Quelldatei main.c.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Figc_HTML.png

    Die #line (...) Angaben erlauben es dem Compiler, später jede Zeile der Datei ihrer ursprünglichen Position in ihrer ursprünglichen C Quelldatei zuzuordnen. Das ist zum Beispiel dann relevant, wenn der Compiler Fehler oder Warnungen meldet. Die angezeigte Zeilennummer eines Fehlers oder einer Warnung gibt immer die entsprechende Zeile in der ursprünglichen Quelldatei wieder.

    1.3.3 C Compiler

    Die Ausgabe des Präprozessors wandert in den Compiler, der daraus Prozessor-spezifischen Maschinencode, also eine dem C-Code entsprechende Datei mit Maschinenbefehlen erzeugt. Die Speicheradressen der Funktionen, Variablen, Sprungadressen etc. werden zu diesem Zeitpunkt noch nicht festgelegt, sondern symbolisch festgehalten.

    Die Ausgabe des Compilers – in diesem Fall des TASKING Compilers für den Infineon AURIX Prozessor – ist auszugsweise in Listing 1.4 zu sehen. Da dieser Code als Eingabe für die nachfolgende Stufe, den Assembler, fungiert, wird er auch Assemblercode genannt.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Figd_HTML.png

    1.3.4 Codeoptimierung durch den Compiler

    Beim Übersetzen von Quellcode in Maschinencode kann ein Compiler eine Vielzahl von Optimierungen vornehmen. Viele dieser Optimierungen verkleinern den Speicherbedarf und führen gleichzeitig zu schnellerem Code. Bei einigen Optimierungen geht eine Verbesserung des einen Aspektes aber nur auf Kosten des anderen. Hier muss der Entwickler entscheiden, was wichtiger ist.

    Der tatsächliche Nutzen einer Optimierung lässt sich oft nur schwer im Vorfeld abschätzen. Wenn es darauf ankommt, muss auf jeden Fall das Ergebnis kontrolliert werden. Dies geschieht am besten indem für die unterschiedlichen Compilereinstellungen a) der generierte Maschinencode verglichen wird und b) vergleichende Messungen durchgeführt werden. Selbst Experten erleben hier immer wieder Überraschungen. Abschn. 8.​3 „Laufzeitoptimierung auf der Codeebene" beschäftigt sich mit dem Thema im Detail.

    1.3.5 Assembler

    Der Assembler übersetzt die textuellen Maschinenbefehle des Assemblercodes in deren binäre Entsprechung. Somit ist die Ausgabe des Assemblers nicht mehr einfach von Menschen lesbar und wird hier nicht weiter dargestellt.

    Aus der Assemblerdatei (meist mit Dateiendung .src oder .s) wird eine Objektdatei. Diese wird oft schlicht  Objekt genannt. Wie zuvor im Assemblercode sind auch im Objektcode die Speicheradressen der Funktionen, Variablen, Sprungadressen etc. noch nicht festgelegt sondern liegen weiterhin ausschließlich symbolisch vor.

    1.3.6 Linker

    Der Linker baut alle ihm übergebenen Objekte zu einem fast fertigen Programm zusammen; es fehlen lediglich noch die konkreten Adressen. In unserem Beispiel wird ein einzelnes Objekt, nämlich main.o übergeben. Es kommen implizit ein paar weitere Objekte hinzu, zum Beispiel cstart.o für die grundlegende Initialisierung, die vor der Ausführung der main() Funktion erforderlich ist. Dazu gehört die Initialisierung des Speicherinterfaces, das Setzen des Stackpointers auf den Stackanfang und die Initialisierung von Variablen.

    Darüber hinaus können dem Linker Funktionsbibliotheken übergeben werden, die typischer Weise an der Dateiendung .a oder .lib zu erkennen sind. Funktionsbibliotheken sind praktisch nichts anderes als Sammlungen von Objekten. In der Abb. 1.3 ist der  Archiver zu erkennen, der ausgewählte Objekte in Archive packt – ganz ähnlich einem Kompressionsprogramm („ZIP") oder einem Tarball Erzeuger.

    Eine weitere Aufgabe des Linkers ist es, alle referenzierten Symbole aufzulösen. Angenommen die main Funktion aus dem Beispiel würde eine weitere Funktion SomeOtherFunction aufrufen, die zuvor mittels einer extern Deklaration bekannt gemacht worden wäre. Diese im Englischen Forward-declaration genannte Bekanntmachung könnte zum Beispiel so aussehen: int SomeOtherFunction(int someParam);.

    Wird diese Funktion nun nicht in main.c implementiert, merkt der Linker sich das Symbol SomeOtherFunction als eines, das referenziert aber noch nicht definiert, also aufgelöst, wurde. In allen weiteren dem Linker übergebenen Objekten sucht der Linker nun nach dem Symbol SomeOtherFunction. Findet er eine Definition, also eine Implementierung der Funktion, ist die Referenz auf das Symbol aufgelöst. Nachdem alle Objekte für die Auflösung von Referenzen durchsucht wurden, werden die beim Aufruf des Linkers übergebenen Funktionsbibliotheken herangezogen, um die verbleibenden Referenzen aufzulösen.

    Bleibt die Suche nach einem Symbol auch hier erfolglos, meldet der Linker einen Fehler, typischerweise „unresolved external

    $$<Symbolname>$$

    ".

    Wird ein Symbol in mehr als einem Objekt definiert, meldet der Linker ebenfalls einen Fehler, in diesem Fall „redefinition of symbol

    $$<Symbolname>$$

    ".

    Wird ein Symbol in einem Objekt und in einer oder in mehreren Funktionsbibliotheken definiert, ignoriert der Linker die Definitionen des Symbols in den Funktionsbibliotheken und meldet weder eine Warnung noch einen Fehler.

    Die Reihenfolge, in der Funktionsbibliotheken dem Linker übergeben werden, bestimmt die Suchreihenfolge. Wird ein Symbol aufgelöst, werden alle nachfolgenden Definitionen ignoriert und nicht „gelinkt".

    1.3.7 Locator

    Die allermeisten Werkzeughersteller fassen Linker und Locator in einem Werkzeug, das dann als Linker bezeichnet wird, zusammen. Die Rolle des Locators ergibt sich aus seinem Namen: er „lokatiert" (verortet) alle Symbole in den verfügbaren Speichern. Damit erfolgt die Festlegung der Speicheradressen für jedes einzelne Symbol.

    Die Ausgabe des Locators ist schließlich das Executable in einem Format mit oder ohne Symbolinformationen. Diese sind unter anderem für das bequeme Debuggen der Software erforderlich. So erlauben die Symbolinformationen zum Beispiel beim Anzeigen des Inhalts von Variablen einfach den Namen der gewünschten Variable anzugeben. Das unhandliche Hantieren mit Speicheradressen ist nicht erforderlich.

    Typische Ausgabeformate für  das Executable ohne Symbolinformationen sind Intel HEX Dateien (*.hex) oder Motorola S-Records (*.s19). Am weitesten verbreitet für die Ausgabe des Executables mit Symbolinformationen ist das ELF Format (*.elf). ELF steht für „Executable and Linking Format".

    Neben dem Executable kann eineLinker-Map, auch Mapdatei oderMapfile genannt, erstellt werden. Unter anderem beinhaltet diese Datei eine Liste aller Symbole nebst deren Speicheradresse.

    1.3.8 Linkerskript

    Eine sehr wichtige Rolle fällt dem Linkerskript (auch Linker Control File) zu. Genau genommen müsste es „Locatorskript oder „Locator Control File heißen, doch fassen – wie bereits erwähnt – die meisten Hersteller Locator und Linker als „Linker" zusammen.

    Listing 1.5 zeigt auszugsweise das Linkerskript für einen einfachen 8 Bit Mikrocomputer, den Microchip AVR ATmega32 mit 32 KByte Flash, 2 KByte RAM und 1 KByte EEPROM.

    Das Linkerskript teilt dem Locator mit, wie die Symbole auf die verschiedenen Speicher des Mikroprozessors zu verteilen sind. In der Regel läuft dies wie folgt ab. Zunächst werden im C- oder Assemblerquellcode alle Symbole bestimmten  Sections – genauer gesagt:  Inputsections – zugewiesen. Diese Zuweisung erfolgt auch dann, wenn der Programmierer sie nicht explizit vornimmt, sie erfolgt dann implizit. Dabei haben sich die folgenden Sectionnamen eingebürgert, welche die Defaultsections benennen.

    .text

    Programmcode

    Beispiel:     int GiveMe42(void){return 42;}

    .rodata

    nur lesbare (read-only) Daten

    Beispiel:     const int a = 5;

    .bss

    les- und schreibbare (read-write) Daten, mit 0 initialisiert

    Beispiel:     int a;

    Entsprechend dem C Standard müssen nicht initialisierte globale Variablen durch den Startupcode mit 0 initialisiert werden. Nicht alle Embedded Softwareprojekte implementieren den Startupcode in dieser Art, sodass man sich nicht darauf verlassen sollte, dass alle bei der Definition nicht initialisierten Variablen beim Startup tatsächlich auf 0 gesetzt werden.

    .data

    les- und schreibbare (read-write) Daten, initialisiert mit einem bestimmten Wert

    Beispiel:     int a = 5;

    .noinit

    les- und schreibbare (read-write) Daten, nicht initialisiert

    Beispiel:     int a;

    Das Beispiel ist identisch mit dem bei .bss. Über Compilerschalter lässt sich meist steuern, ob im Code nicht initialisierte Variablen mit 0 oder gar nicht initialisiert werden sollen.

    .debug

     Debugsections beinhalten weder Code noch Daten des Programms sondern Zusatzinformationen, die das Debuggen der Software ermöglichen beziehungsweise vereinfachen. Abschn. 1.3.9 befasst sich mit diesem Thema und geht näher auf Debugsections ein.

    Es hat sich etabliert, dass Sectionnamen mit einem Punkt beginnen.

    Die Anweisungen im Linkerskript weisen im nächsten Schritt alle Inputsections Outputsections zu, die ihrerseits schließlich auf die verfügbaren Speicher abgebildet werden. Zum besseren Verständnis finden sich für die .text Sections im Listing 1.5 entsprechende Kommentare.

    In einem klassischen Linkerskript finden sich wie in Listing 1.5 am Anfang die Definitionen der verfügbaren Speicher. Dann folgen die Definitionen der Outputsections und mit jeder dieser Definitionen die Verknüpfung mit den Inputsections sowie die Zuweisung zu einem Speicher.

    Eine sehr gute Beschreibung der Syntax dieses Linkerskripts sowie die zugrunde liegenden Konzepte finden sich im Handbuch des GNU Linkers [2]. Die meisten anderen Werkzeughersteller haben für ihre Linker zumindest die Konzepte des GNU Linkers („ld") übernommen, oft sogar die Syntax des Linkerskripts kopiert.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Fige_HTML.png

    Es mag verwirrend sein, dass Inputsections, Outputsections und sogar die Speicher die gleichen Namen haben dürfen, siehe eeprom am Ende von Listing 1.5, dennoch ist dies möglich und sogar üblich.

    Was hat nun das Linkerskript mit dem Thema Timing zu tun? Wie wir in den Abschn. 2.​4 „Wait-states, Burstzugriffe und 2.​3 „(Speicher-) Adressierung, Adressierungsart sehen werden, haben Speicherort und Zugriffsart einen wesentlichen Einfluss auf die Zugriffsdauer und damit auf die Laufzeit des zugreifenden Codes. Die Festlegung von Speicherort und Zugriffsart erfolgt über das Linkerskript und somit ist die Kenntnis dessen Syntax und Funktionsweise essentiell für die Laufzeitoptimierung. Abschn. 8.​2 „Laufzeitoptimierte Speichernutzung" geht im Detail auf dieses Thema ein.

    1.3.9 Debugger

    In der Softwareentwicklung hat sich der englische Begriff „bug (Insekt; Ungeziefer) für Softwarefehler etabliert. Demnach ist der „Debugger das Werkzeug, welches den Entwickler dabei unterstützt, Softwarefehler zu eliminieren. Darüber hinaus erfüllt er weitere wichtige Aufgaben wie zum Beispiel das Flashen des Executables in den Programmspeicher während der Entwicklungsphase.

    Der Debugger gehört zwar nicht zu den Werkzeugen, die zum bauen des Executables erforderlich ist, doch rundet er den Abschnitt „Buildprozess" sehr schön ab. Im Verlauf des Abschnitts wurde der Weg vom Modell oder vom Quellcode zum Executable skizziert. Modell oder Quellcode befinden sich auf einer hohen Abstraktionsebene, das eigentliche Executable auf einer niedrigen – es besteht ja nur noch aus Einsen und Nullen im Programmspeicher. Die verschiedenen Abstraktionsebenen sind auch in Abb. 1.3 ersichtlich: der Buildprozess verläuft von oben nach unten.

    Der Debugger geht gewissermaßen den umgekehrten Weg: er verbindet sich mit dem Prozessor und in der Regel liest er auch das Executable – meist im ELF Format – ein. Wenn verfügbar, kann auch der Quellcode geladen werden, was das Debuggen erheblich vereinfacht. Schritt für Schritt beziehungsweise Befehl für Befehl kann der Debugger den Prozessor Code abarbeiten lassen und zeigt für jeden Schritt die aktuelle Position im Programmcode an. Darüber hinaus wird sich der Entwickler auch den Inhalt bestimmter Speicherbereiche, Variablen oder aller Register darstellen lassen. Abb. 1.4 zeigt eine solche Debugsituation unter Verwendung des Codebeispiels, das schon in den Abschnitten zum Compiler, Assembler etc. herangezogen wurde.

    Um jeder gültigen Adresse im Programmspeicher die passende Zeile im Quellcode zuordnen zu können, greift der Debugger auf die  Debuginformationen zu, die in den bereits erwähnten Debugsections abgelegt sind. Ein Großteil der Debuginformationen liegen im DWARF Format innerhalb der ELF Datei vor. Ursprünglich ist DWARF keine Abkürzung sondern eine spielerische Anlehnung an ELF: Englisch „elf ist im Deutschen die Elfe und Englisch „dwarf ist im Deutschen der Zwerg.

    Weniger märchenhaft aber sehr praktisch sind die Möglichkeiten, welche die Debuginformationen eröffnen. Sie stellen gewissermaßen die Brücke vom Executable zu den Quellen dar – von einer niedrigen Abstraktionsebene zu einer hohen. Der Prozessor arbeitet mit Einsen und Nullen und dennoch kann der Debugger den dazugehörigen Quellcode darstellen. Die DWARF Informationen der ELF Datei geben dem Debugger zu jeder Speicheradresse die dazugehörige Quelldatei und Zeilennummer an.

    Weitere Beispiele für DWARF Informationen sind Typinformationen für Variablen oder Details zum Aufbau von structs.

    Das Konzept, dass Debuginformationen Aufschluss darüber geben, zu welcher Quelle auf einer höheren Ebene ein Befehl auf einer unteren Ebene gehört, ist allgemein sinnvoll. Es muss nicht auf der Ebene des C Quellcodes enden. Wenn ein Codegenerator den erzeugten C Code mit Debuginformationen anreichert, kann danach prinzipiell das Debuggen auch auf der Modellebene erfolgen. Der Entwickler kann dann beispielsweise schrittweise Blöcke ausführen und bekommt die Werte von Eingangsgrößen direkt an den entsprechenden Eingängen in der richtigen physikalischen Skalierung mit entsprechender Einheit angezeigt.

    ../images/478274_1_De_1_Chapter/478274_1_De_1_Fig4_HTML.png

    Abb. 1.4

    Screenshot des TRACE32 Debuggers mit Maschinencode, Assemblercode und C-Code

    1.3.9.1 Erläuterung der Debuggeransicht

    Wie bereits erwähnt zeigt Abb. 1.4 das Programm aus Listing 1.1. Im großen Fenster ist in blauer Schrift der C Quellcode und in grüner Schrift der disassemblierte Maschinencode inklusive der Sprungmarken, auch Labels genannt, zu sehen. Labels beginnen meist mit einem Punkt, hier zum Beispiel .L35 oder .L36. Der disassemblierte Maschinencode entspricht dem Assemblercode, siehe Listing 1.4 und wird in der Spalte „mnemonic dargestellt.  Unter „Mnemonic – dem englischen Begriff für Gedächtnishilfe – versteht man den sich einfach zu merkenden Namen für einen Maschinenbefehl, beispielsweise ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq17_HTML.gif für die Addition. Die zusätzliche „16" an manchen Befehlen zeigt an, dass es sich hier um einen 16 Bit Befehl handelt, wie ihn die Infineon TriCore Architektur unterstützt.

    Im grau hinterlegten Bereich links steht vor jeder Zeile mit Maschinencode die jeweilige Programmspeicheradresse und vor jeder Zeile mit Quellcode die jeweilige Zeilennummer der Quellcodedatei. In grauer Schrift jeder Programmspeicheradresse steht der Inhalt, der Opcode. Sehr schön erkennbar ist, dass die 16 Bit Befehle tatsächlich 16 Bits veranschlagen (zum Beispiel 0x129A für add16 d15,d2,#0x1) und 32 Bits für alle restlichen (zum Beispiel 0x0000FF59 für st.w [a15]0x0,d15).

    Unten links im Screenshot ist die Registeransicht zu sehen. Der  PC (Englisch „program counter) zeigt auf die Programmspeicheradresse des als nächstes zu bearbeitenden Befehls, hier 0x80000468. Diese Adresse ist in der Codeansicht mit einem grauen Balken hinterlegt, sodass man direkt sieht, „wo der Prozessor gerade steht.

    In dem „Watch-Fenster" in der Mitte rechts werden aktuell zwei Ausdrücke beobachtet: zum einen der Wert der Variablen a zum anderen deren Speicheradresse &a. Es lassen sich hier tatsächlich sogar komplexe C Ausdrücke angeben, was vielen Entwicklern nicht bewusst ist, was aber sehr hilfreich sein kann.

    Schließlich zeigt die Speicheransicht unten rechts einen Ausschnitt des RAM Speichers an. Der angezeigte Bereich beginnt bei Adresse 0x70003FF8, der Adresse der Variablen a. Und tatsächlich: der Inhalt ist 2A hexadezimal, was 42 dezimal entspricht. Offenbar wurde die Funktion GetSomeValue() genau einmal aufgerufen (oder vielleicht doch

    $$n \cdot 2^{32} + 1$$

    mal?). Nach der nächsten Ausführung dieser Funktion und dem Speichern des Ergebnisses in der Variablen a mittels des Maschinenbefehls st16.w [a10]0x0,d2 an der Programmspeicheradresse 0x8000046C (Label .L41) wird die Variable den Wert 43 haben.

    1.4 Zusammenfassung

    Im Kapitel Allgemeine Grundlagen wurde empfohlen, bestehende Entwicklungsprozesse um Aspekte des Embedded Software Timings zu erweitern. Wie dies konkret aussehen kann, wurde anhand des V-Modells gezeigt. Eine weitere Detaillierung der Entwicklungsschritte mit Timingbezug wird im weiteren Verlauf des Buches erfolgen.

    Weiterhin wurden die einzelnen Stufen und Werkzeuge im Buildprozess näher beleuchtet. Ein besonderes Augenmerk wurde dabei auf das Linkerskript gerichtet, da es einerseits bei der Laufzeitoptimierung eine wichtige Rolle spielt und es andererseits erfahrungsgemäß stiefmütterlich behandelt wird und dann Probleme bei der Entwicklung verursacht.

    Literatur

    1.

    Wikipedia Artikel „V-Modell (Entwicklungsstandard)", 2020https://​de.​wikipedia.​org/​wiki/​V-Modell_​(Entwicklungsstan​dard)

    2.

    Steve Chamberlain, Ian Lance Taylor The GNU linker, Abschnitt 3.1 „Basic Linker Script Concepts", 2010https://​www.​eecs.​umich.​edu/​courses/​eecs373/​readings/​Linker.​pdf

    © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021

    P. GliwaEmbedded Software Timinghttps://doi.org/10.1007/978-3-658-26480-2_2

    2. Mikroprozessortechnik Grundlagen

    Peter Gliwa¹  

    (1)

    Gliwa GmbH, Weilheim, Bayern, Deutschland

    Peter Gliwa

    Email: peter.gliwa@gliwa.com

    Elektronisches Zusatzmaterial

    Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht https://​doi.​org/​10.​1007/​978-3-658-26480-2_​2.

    Der Mikroprozessor – oft wird auch im Deutschen der englische Begriff „Microcontroller oder nur „Controller verwendet – ist die Hardwareeinheit, auf der Embedded Software zur Ausführung kommt. Ohne das Verständnis des prinzipiellen Aufbaus von Mikroprozessoren ist eine Optimierung des Timings von Embedded Software kaum möglich.

    Dieses Kapitel vermittelt die Grundlagen der Mikroprozessortechnik mit Blick auf die für das Timing relevanten Aspekte. Zusammen mit dem Datenblatt des jeweils verwendeten Mikroprozessors bildet es die notwendige Grundlage für die Entwicklung effizienter Embedded Software. An vielen Stellen werden allgemeine Grundlagen durch konkrete Beispiele verdeutlicht. Die für die Beispiele verwendete Spanne an Mikroprozessoren reicht dabei von kleinen 8 Bit Controllern bis 32 Bit Prozessoren, von Single- bis Multicore.

    Nachfolgende Kapitel, insbesondere Kap. 7 „Timing bei Multicore, Manycore, Multi-ECU und Kap. 8 „Laufzeitoptimierung, setzen die Grundlagen der Mikroprozessortechnik voraus.

    2.1 Aufbau von Mikroprozessoren

    Abb. 2.1 zeigt den schematischen Aufbau eines Mikroprozessors, hier am Beispiel eines Dualcoreprozessors, also eines Mikroprozessors mit zwei Kernen.

    Dargestellt sind die beiden Rechenkerne, verschiedene Speicher, Peripherieeinheiten sowie zwei verschiedene Busse. Nicht alle Prozessoren folgen diesem Aufbau. Die Allgemeingültigkeit steht bei dieser Darstellung weniger im Mittelpunkt als eine einfache Verständlichkeit. Für den Begriff „Rechenkern wird im Folgenden meist nur „Kern oder CPU (Central Processing Unit, zentrale Recheneinheit) verwendet.

    ../images/478274_1_De_2_Chapter/478274_1_De_2_Fig1_HTML.png

    Abb. 2.1

    Blockdiagram eines Dualcoreprozessors

    2.1.1 CISC vs. RISC

    CISC steht für Complex Instruction Set Computer und beschreibt Prozessoren, die komplexe Befehle mit vergleichsweise viel Funktionalität bieten. Für die Umsetzung wird eine entsprechend komplexe und somit auch kostspielige Hardware benötigt. Weiterhin haben CISC die Eigenart, dass die Befehle unterschiedlich lange für die Ausführung brauchen.

    RISC steht für Reduced Instruction Set Computer. Die Maschinenbefehle solcher Prozessoren sind einfach, benötigen wenige Transistoren und veranschlagen für die Ausführung meist die gleiche Zeit.

    2.1.2 Register

    Jeder Prozessor hat in seiner Ausführeinheit (Englisch „execution unit") einen Satz spezieller Speicherzellen, die sogenannten Register. Im Folgenden sollen einige der Register genauer beschrieben werden.

    Befehlszeiger

    Der Befehlszeiger (Englisch „program counter, im Folgenden PC) wird oft auch „instruction pointer (IP) genannt. Jeder Befehl im Programmspeicher hat eine bestimmte (Speicher-) Adresse. Der PC enthält die Adresse des Befehls, der gerade abgearbeitet wird. Weitere Details der Befehlsausführung werden im folgenden Abschnitt „(Speicher-) Adressierung, Adressierungsart" behandelt.

    Datenregister

    Die Datenregister werden für logische Operationen, zum Rechnen und für Lese- und Schreiboperationen aus den Speichern beziehungsweise in die Speicher herangezogen. In dem Codebeispiel, das in Abb. 1.​4 ersichtlich ist, addiert der Prozessor 1 zum Wert in Datenregister d2 und speichert das Ergebnis in Datenregister d15. Siehe den Code bei Label .L35:

    add16 d15,d2,#0x1

    Nach der Addition wird im nächsten Befehl der Inhalt von d15 in den Speicher geschrieben.

    Akkumulator

    Insbesondere RISC Prozessoren haben ein besonderes Datenregister, den Akkumulator, der für die meisten Logik- und Rechenoperationen herangezogen wird.

    Adressregister

    Adressregister werden verwendet, um Daten von Speichern zu lesen, Daten in Speicher zu schreiben, indirekte Sprünge auszuführen oder Funktionen indirekt aufzurufen. Im folgenden Abschnitt „(Speicher-) Adressierung, Adressierungsart" werden Sprünge und Funktionsaufrufe näher behandelt.

    Nicht alle Prozessoren unterscheiden zwischen Adressregistern und Datenregistern.

    Statusregister

    Das Statusregister wird auch als „program status word (PSW), „condition code register (CCR) oder „flag register bezeichnet. Es ist eine Ansammlung von Bits, die jeweils einen bestimmten Zustand anzeigen. Jedes Bit fungiert wie eine Flagge (Englisch „flag) und wird meist mit einem oder zwei Buchstaben abgekürzt. Welche Zustände das im einzelnen sind, ist abhängig vom verwendeten Prozessor, wobei die folgenden Flags bei den meisten Architekturen anzutreffen sind.  

    IE, Interrupt Enable Flag

    zeigt an, ob Interrupts generell freigegeben ( $$IE=1$$ ) oder generell gesperrt sind ( $$IE=0$$ ). Damit Interrupts zum Zuge kommen, müssen weitere Voraussetzungen erfüllt sein. Abschn. 2.7 geht näher auf Interrupts ein.

    IP, Interrupt Pending Flag

    zeigt an, ob ein Interrupt zur Bearbeitung ansteht ( $$IP=1$$ ) oder ob kein Interrupt „pending" ist ( $$IP=0$$ ).

    Z, Zero Flag

    zeigt an, ob das Ergebnis der zuletzt ausgeführten logischen oder arithmetischen Funktion Null war ( $$Z=1$$ ) oder nicht ( $$Z=0$$ ).

    C, Carry Flag

    wird für Überläufe beziehungsweise Überträge bei Rechenoperationen sowie bei logischen Operationen herangezogen. Werden beispielsweise bei einem 8 Bit Prozessor die beiden Zahlen 0xFF und 0xFF addiert, repräsentiert das Carry Flag das neunte Bit. Die führende „1", das MSB (most significant bit) des Ergebnisses 0x1FE steht im Carry Flag während die restlichen acht Bits 0xFE im Ergebnisregister landen.

    Bei einer Addition mit Carry dagegen wird das Carry Flag wie ein Übertrag aus einer vorherigen Addition verwendet. Ist das Carry Flag gesetzt und wird eine solche Addition der beiden Zahlen 3 und 4 vorgenommen, ist das Ergebnis somit 8.

    Aktuelle CPU Priorität

    Dieser Wert (im Englischen die „current CPU priority") findet sich bei einigen Prozessoren als eigenes Register wieder, bei anderen ist er Teil des Staturegisters. Die Priorität des aktuell ausgeführten Codes entscheidet darüber, ob ein Interrupt angenommen wird oder nicht. Nur wenn der Interrupt eine höhere Priorität als die des aktuell ausgeführten Codes aufweist, unterbricht er den aktuell ausgeführten Code – vorausgesetzt, Interrupts werden generell zugelassen ( $$IE=1$$ ).

    Stackpointer

    Der Stackpointer beinhaltet eine Adresse, welche die aktuelle Grenze des bisher benutzten Stacks markiert.

    2.2 Codeabarbeitung

    Abschn. 1.​3 Buildprozess: vom Modell zum Executable hat gezeigt, wie der ausführbare Maschinencode erzeugt wird und dass es  sich bei diesem Code um eine Ansammlung von Maschinenbefehlen handelt. Der Rechenkern eines Mikroprozessors arbeitet fortwährend Maschinenbefehle ab. Dazu werden diese Befehle sequentiell vom Programmspeicher (Englisch „program-memory oder „code-memory) in die Ausführungseinheit geladen, dort dekodiert und dann ausgeführt.

    Vom Befehlszeiger (PC) war bereits die Rede, er „zeigt" quasi auf den aktuellen Befehl im Programmspeicher. Solange keine Sprungbefehle oder Befehle für einen (Unter-) Funktionsaufruf vorliegen, wird mit jeder Abarbeitung eines Befehls der PC um eine Speicherstelle erhöht. Dadurch zeigt der PC auf den nächsten Befehl, der wiederum in die Ausführungseinheit geladen, dort dekodiert und ausgeführt wird. Der Programmspeicher ist in erster Linie eine Aneinanderreihung von Maschinenbefehlen.

    An der Stelle sei schon einmal erwähnt, dass eine Serie von Maschinenbefehlen ohne Sprung Basisblock (Englisch „basic block") genannt wird. Genauer: ein Basisblock ist eine Serie von Maschinenbefehlen, in die nicht hineingesprungen wird und aus der nicht heraus gesprungen wird. Die Befehle eines Basisblocks werden also ausnahmslos alle sequentiell beginnend mit dem ersten Befehl abgearbeitet. Basisblöcke spielen unter anderem bei der statischen Codeanalyse eine wichtige Rolle, daher wird das Thema später noch einmal aufgegriffen werden.

    Die Befehle, die ein Prozessor bietet, sind im Handbuch des Befehlssatzes (Englisch „Instruction Set Reference") des Prozessors beschrieben. Für eine weitgehende Optimierung von Software auf der Codeebene ist die Kenntnis des Befehlssatzes des jeweiligen Prozessors unerlässlich. Abschn. 8.​3 „Laufzeitoptimierung auf der Codeebene" wird im Detail darauf eingehen.

    ../images/478274_1_De_2_Chapter/478274_1_De_2_Fig2_HTML.png

    Abb. 2.2

    Auszug aus der Befehlsreferenz des AVR ATmega Prozessors [1]

    Am Beispiel eines Addierbefehls beim 8 Bit Microchip AVR Prozessor soll die Codierung eines Befehls und der Umgang mit der Dokumentation des Befehlssatzes veranschaulicht werden. Microchip AVR Prozessoren haben 32 Daten-/Adressregister. Abhängig vom Befehl dienen sie mal als Datenregister, mal als Adressregister. Abb. 2.2 zeigt einen Auszug (genau eine Seite) aus der Befehlsreferenz des Microchip AVR ATmega Prozessors [1], nämlich den Abschnitt, der den Addierbefehl mit Carry Flag beschreibt. Der Beschreibung in textueller und formaler Form (

    $$Rd \leftarrow Rd + Rr + C$$

    ) folgt die Definition der Syntax. Dies ist der Befehl in genau der Schreibweise, wie sie sich auch im Assemblercode findet. Ein solcher „Assemblercodebefehl" wird auch Mnemonic  genannt (Englisch für „Gedächtnisstütze"). Die Tabelle darunter lässt den Opcode des Befehls erkennen, also durch welchen Wert im Speicher der Befehl repräsentiert wird. In diesem Fall sind die sechs höchstwertigen Bits fest (binär 000111) und die restlichen zehn Bits definieren, welche Register miteinander addiert werden sollen: die mit „d" bezeichneten Bitpositionen für Register Rd, die mit „r" bezeichneten für Register Rr. Beispiel: sollen die Register R3 und R22 addiert werden und soll das Ergebnis in R3 gespeichert werden, sieht der Opcode wie in Listing 2.1 dargestellt aus. Wann immer im Assemblercode adc r3,r22 zu finden ist, wird an der entsprechenden Stelle im Programmspeicher eine 0x1D63 stehen.

    ../images/478274_1_De_2_Chapter/478274_1_De_2_Figa_HTML.png

    Tipp

    Die im Listing 2.1 dargestellten Kommentare zur Erläuterung der Bitcodierung haben sich bei der Programmierung sehr gut bewährt. Wann immer Informationen binär codiert sind und der Erklärung bedürfen, sind Kommentare dieser Art äußerst hilfreich. Die Bitposition wird über zwei Kommentarzeilen kenntlich gemacht: eine mit den Zehnern und eine mit den Einern. Die Bitposition wird nun einfach von oben nach unten gelesen, zum Beispiel

    Gefällt Ihnen die Vorschau?
    Seite 1 von 1