39,90 €
Niedrigster Preis in 30 Tagen: 39,90 €
Dein Einstieg in Embedded Systems mit RISC-V - Mit Schritt-für-Schritt-Anleitungen und zahlreiche Abbildungen werden Sie an das Thema herangeführt - Hands-On-Projekte vermitteln Ihnen grundlegende Konzepte und Funktionsweisen - Erhalten Sie einen kostengünstigen Start in die Welt der eingebetteten SystemeNeu und groß im Kommen - RISC-V ist eine auf freier Technologie basierende Befehlssatzarchitektur. In Anwendung im ESP32-C3-DevKitM-1 werden hier grundlegende Konzepte und Funktionsweisen von Mikrocontrollern theoretisch vermittelt. Mehrere Hands-On-Projekte dienen außerdem dazu, Leser*innen einen ersten Einblick zu geben und Sie für das selbstständige Programmieren mit Mikrocontrollern vorzubereiten und das Gelernte Schritt für Schritt zu vertiefen. Das Werk ist in drei Teile gegliedert, welche aufeinander aufbauen. Teil I geht hierbei auf den Aufbau eines RISC-V-Mikroprozessors und die hardwarenahe Programmierung ein. Hierbei werden den Leser:innen wichtige Grundlagen mit auf den Weg gegeben. Der zweite Teil setzt den Fokus auf das Erlernen von elektrotechnischen Grundlagen und wie Peripheriemodule angesteuert werden. Im letzten Teil wird ein Pulsoximeter als Beispiel genommen, wie verschiedene Internetprotokolle funktionieren.
Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:
Prof.(FH) Dipl-Ing. Patrick Ritschel studierte Informatik an der TU Wien. Anschließend leitete er die Entwicklung von Smart Cards bei der Winter AG. Seit 2003 unterrichtet er embedded Systems, Programmierung und Algorithmik in C, C++ und Java, sowie Mobile Computing an der Fachhochschule Vorarlberg. Er gründete die clownfish IT GmbH, die eingebettete Anwendungen im B2B-Bereich anbietet. In seiner Freizeit zieht es ihn mit seiner Familie auf die Theaterbühne, um zu spielen, zu singen und auch Theaterstücke zu schreiben.
Copyright und Urheberrechte:
Die durch die dpunkt.verlag GmbH vertriebenen digitalen Inhalte sind urheberrechtlich geschützt. Der Nutzer verpflichtet sich, die Urheberrechte anzuerkennen und einzuhalten. Es werden keine Urheber-, Nutzungs- und sonstigen Schutzrechte an den Inhalten auf den Nutzer übertragen. Der Nutzer ist nur berechtigt, den abgerufenen Inhalt zu eigenen Zwecken zu nutzen. Er ist nicht berechtigt, den Inhalt im Internet, in Intranets, in Extranets oder sonst wie Dritten zur Verwertung zur Verfügung zu stellen. Eine öffentliche Wiedergabe oder sonstige Weiterveröffentlichung und eine gewerbliche Vervielfältigung der Inhalte wird ausdrücklich ausgeschlossen. Der Nutzer darf Urheberrechtsvermerke, Markenzeichen und andere Rechtsvorbehalte im abgerufenen Inhalt nicht entfernen.
Patrick Ritschel
Eine praktische Einführung in Architektur,Peripherie und eingebettete Programmierung
Patrick Ritschel
www.ritschel.at
Lektorat: Gabriel Neumann
Copy-Editing: Annette Schwarz, Ditzingen
Satz: Patrick Ritschel
Herstellung: Stefanie Weidner, Frank Heidt
Umschlaggestaltung: Helmut Kraus, www.exclam.de
Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
ISBN:
Print978-3-86490-937-5
PDF978-3-96910-998-4
ePub978-3-96910-999-1
mobi978-3-98890-000-5
1. Auflage 2023
Copyright © 2023 dpunkt.verlag GmbH
Wieblinger Weg 17
69123 Heidelberg
Hinweis:
Dieses Buch wurde mit mineralölfreien Farben auf PEFC-zertifiziertem Papier aus nachhaltiger Waldwirtschaft gedruckt. Der Umwelt zuliebe verzichten wir zusätzlich auf die Einschweißfolie. Hergestellt in Deutschland.
Schreiben Sie uns:
Falls Sie Anregungen, Wünsche und Kommentare haben, lassen Sie es uns wissen: [email protected].
Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.
Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.
Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.
5 4 3 2 1 0
»Für euch, Kinder der Wissenschaft und der Weisheit, haben wir dieses geschrieben. Erforschet das Buch und suchet euch unsere Ansicht zusammen, die wir verstreut und an mehreren Orten dargetan haben; was euch an einem Orte verborgen bleibt, das haben wir an einem anderen offengelegt, damit es fassbar werde für eure Weisheit.«
HEINRICH C. AGRIPPA VON NETTESHEIM »DE OCCULTA PHILOSOPHIA«
Warum schreibt man heutzutage noch ein Fachbuch? – Man findet doch alle Informationen im Internet.
Diese Antwort ist grundsätzlich richtig, trifft aber nicht den Kern des Buchbegriffs:
Wenn man ein Buch liest, nimmt man es zur Hand, blättert, liest dies, sieht das. Und irgendwann auch das, was man sucht. Doch der Weg zum Gesuchten ist voller schöner Überraschungen und Informationen. Man profitiert bereits von der Suche, was bei der Internetsuche selten der Fall ist. Dort bekommt man Millionen Treffer, die nicht zu weit vom Thema weg führen.
Ein Buch ist also kein Anachronismus in einer zunehmend digitalisierten Welt, sondern durchaus zeitlos und modern. Ein E-Book erzielt, einen entsprechenden Reader vorausgesetzt, denselben Erfolg wie die Papiervariante.
Entgegen dem strukturierten Aufbau üblicher Fachbücher zu der Thematik habe ich einen beispielorientierten erzählerischen Ansatz gewählt, der entlang eines roten Fadens vom Mikroprozessor zum IoT-Ding führt. So werden viele Themen behandelt, die theoretische und praktische Bedeutung für die Entwicklung professioneller Embedded Systeme haben.
Ich möchte mich an dieser Stelle noch herzlich bei meinen geschätzten Freund:innen und Kolleg:innen bedanken, die mir mit Rat und Tat zur Seite gestanden haben. Dies sind Johannes Koch MSc, der auch Inhalte der Webseite zum Buch beigesteuert hat, Dr. Regine Kadgien, Dr. Franz Geiger, DI Wolfgang Auer und Dr. Christoph Scheffknecht sowie die Teams des dpunkt.verlags und der Firma clownfish GmbH. Des Weiteren möchte ich meinen Lieben, Johanna, Jakob und Babsi, für ihre Geduld danken, mit der sie mich bei all meinen Ideen und Projekten unterstützen.
Ich wünsche der Leserin bzw. dem Leser viel Vergnügen beim Schmökern und Blättern, beim Löten und Messen, beim Grübeln und Programmieren! Und wenn interessante Dinge zu knapp erklärt sind, macht es doch wieder Sinn, sie nachzugoogeln …
IMikrocontrollergrundlagen
1Einleitung
1.1Ziel des Buchs
1.2Struktur des Buches
1.3Zielpublikum
1.4Gebrauchsanweisung
1.4.1Konventionen
2Hallo, Welt!
2.1Wahl der Programmiersprache
2.2Benötigte Komponenten für die Applikationsentwicklung
2.2.1Development Board
2.2.2Software für die Entwicklung
2.3Die erste Applikation
3Der Mikroprozessor
3.1Prozessorarchitektur
3.1.1Eine kleine Aufgabe
3.1.2Die Registerbank
3.1.3Die Arithmetic Logic Unit (ALU)
3.1.4Datenspeicher
3.1.5Befehlsspeicher
3.1.6Steuerwerk
3.1.7Weitere Einheiten
3.1.8Der Prozessor
3.1.9Pipeline
3.2Instruction Set Architecture
3.2.1RISC-V
3.2.2sum_up_n in Assembler
3.2.3sum_up_n-Maschinensprache
3.3Performance
3.3.1Control and Status Registers
3.3.2Funktionsaufruf
3.3.3Optimierung des Codes
3.3.4Änderung des Verfahrens
4Der Mikrocontroller
4.1Aufbau eines Mikrocontrollers
4.1.1Test des Zufallszahlengenerators
4.1.2Das Bussystem
4.1.3ESP32-C3 Memory Map
4.2Speicher
4.2.1Speichertechnologien
4.2.2Speicherzugriffe in Software
4.2.3Cache
4.2.4Linker
4.3Peripheriemodule
4.3.1Peripheriezugriff
4.3.2Durchführung des Zufallszahlentests
4.3.3Informationen der Hersteller
4.3.4Speicherlayout der Peripherie
4.3.5Bits als Schalter
4.4Bitmaskierung
4.4.1Klassische Aussagenlogik
4.4.2Bitweise Operatoren in C
4.4.3Bitmaskierung
4.5Zusammenfassung
IIPeripheriemodule
5Digitale Ein-/Ausgabe
5.1Peripherie
5.2Projekt Pulsoximeter
5.3Elektrotechnische Grundlagen
5.3.1Strom und Spannung
5.3.2Widerstand und Ohm’sches Gesetz
5.3.3Halbleiter und Diode
5.3.4Schaltungsaufbau »LED an Batterie«
5.4LED schalten
5.4.1Transistor
5.4.2Logische Funktionen mit CMOS
5.4.3GPIO-Modul
5.4.4Schaltungsaufbau ESP32-C3 mit LEDs
5.4.5Pin-Multiplexing
5.4.6Set-/Reset-Register
5.4.7Bitfeld und Union in C
5.4.8Gesamtes Modul kapseln
5.4.9API des Herstellers
5.4.10Oszilloskop als Hilfsmittel
5.4.11Kondensator
5.4.12Leistung, Arbeit, Batterielebensdauer
5.5Taster anschließen
5.5.1GPIO Eingangssignalpfad
6Interrupts und Exceptions
6.1Exceptions und Interrupts
6.1.1RISC-V-Ausnahmebehandlung
6.1.2Aktivierung des Interrupts
6.1.3Exception Handler
6.2Schichtenarchitektur und Callback
6.2.1Schichtenarchitektur
6.2.2Callbacks
6.3Interrupt bei Tastendruck
6.4Sourcecodeverwaltung
6.4.1Module in Unterverzeichnissen
6.4.2Komponentenmodell des ESP-IDF
6.4.3Versionsverwaltung
7Externe Komponenten digital anschließen
7.1Display ansteuern
7.2Konfiguration im ESP-IDF
7.3I2C-Protokoll
7.3.1SMBus
7.4SPI-Schnittstelle
7.4.1Bit-Banging
7.4.2DMA: Direct Memory Access
7.4.3Dateispeicherung auf SD-Karten
7.5WS2812B
7.6Weitere Kommunikationsschnittstellen
7.6.1Serielle Schnittstelle, RS-232
7.6.2I2S
7.6.3CAN
7.6.4Funkschnittstellen
8Analoge Werte verarbeiten
8.1Die Welt ist analog
8.1.1Abtastung (Sampling)
8.1.2Analog-Digital-Wandlung
8.1.3Messen am Spannungsteiler
8.2Werte filtern
8.2.1Filterimplementierung
8.3Den Herzschlag erkennen
8.3.1Diskrete Fourier-Transformation
8.4Die Zeit messen
8.4.1Taktgeber
8.5Das Timer-Modul
8.5.1Timer des ESP32-C3
8.5.2Systemzeit und Kalenderzeit
8.5.3Zeitsynchronisierung
8.5.4Pulsweitenmodulation (PWM)
8.5.5Weitere Komponenten
8.6Zusammenfassung
IIIEmbedded System
9Embedded Betriebssystem
9.1Embedded Applikationsmodell
9.2Multitasking
9.3Echtzeitbetriebssystem
9.3.1FreeRTOS
9.4Nebenläufigkeit
9.4.1Semaphor
9.4.2Kritische Region
9.4.3Deadlock
9.4.4Producer/Consumer
9.4.5Message-Queue
9.4.6Mutex und Signalisierung
9.4.7Prioritätenbasiertes Scheduling
9.5Systemkontext
9.6Gerätetreiber
9.6.1POSIX-Standard
10Internet der Dinge
10.1Internet
10.1.1Wi-Fi-Konfiguration
10.1.2Berkeley Sockets
10.1.3UDP
10.1.4TCP
10.1.5Datenformate
10.1.6Header
10.2Cloud-Zugriff
10.2.1REST und CoAP
10.2.2MQTT-Protokoll
10.2.3Webserver
10.3Bluetooth
10.3.1NimBLE Stack
10.3.2Generic Access Profile (GAP)
10.3.3GATT-Profil und ATT-Protokoll
10.4Power-Management
10.4.1Sleep Modes
10.4.2Power-Management-Algorithmus
11Schlusswort
IVAnhang
AWebseite zum Buch
A.1Material zum ESP32-C3 und ESP-IDF
A.2Beispiele des Buchs
A.3Übungsbeispiele
A.4Errata
Literaturverzeichnis
Index
»Kompliziertes kompliziert zu sagen ist einfach.
Nur Einfaches einfach zu sagen ist kompliziert.«
KARL-HEINZ KARIUS
Laut statista.com [57] wurden im Jahre 2021 weltweit 31,2 Milliarden Mikrocontroller produziert, was rund vier neuen Mikrocontrollern pro Erdenbürger entspricht. Damit sind diese Kleinstcomputer mittlerweile auch in Gegenständen des Alltags verbaut (»eingebettet«, embedded), in denen wir sie nicht vermuten. Oft werden Consumer-Produkte, Haushalts- und Kommunikationsgeräte, die Mikrocontroller enthalten, als »smart« bezeichnet. Vom üppig ausgestatteten Smartphone bis zur stark ressourcenbeschränkten Smart Card ist ein Mikroprozessor informationsverarbeitender Kern des Gerätes. All diese Geräte benötigen eine weitestgehend fehlerfreie Software, um mitunter ohne Update-Möglichkeit viele Jahre reibungslos zu funktionieren. Um dies zu gewährleisten, ist ein solides Grundverständnis von Aufbau und Arbeitsweise der Embedded Systeme unerlässlich.
Da die Komplexität durch wachsende Applikationsgröße und Internetanbindung steigt, wird auch die Programmentwicklung umfangreicher und komplexer. Diese IoT(»Internet of Things«)-Geräte besitzen embedded Betriebssysteme, deren Tasks miteinander kommunizieren. Auch dieses Zusammenspiel muss gut durchdacht sein, um performante Software ohne Deadlocks zu designen.
Ein Einstieg in die Entwicklung eingebetteter Systeme (in der Folge »Embedded Systeme« genannt) bringt verschiedene Hürden mit sich. Oft ist eine Programmiersprache bekannt, doch die technischen Details machen die Lernkurve steil und den Weg zum Ziel steinig.
Nimmt man ein Buch über die C-Programmiersprache zur Hand, enthält dieses keine Details zur Programmierung von Mikrocontrollern. Versucht man hingegen, die Datenblätter, Family Guides und Reference Manuals zu verwenden, machen die technischen Details den Einstieg schwer. Die Beispielprogramme und Application Notes auf den Webseiten der Hersteller implementieren Lösungen für sehr spezielle Probleme, was eine Umsetzung einer Lösung zu einem anders gearteten Problem schwierig gestaltet.
Insgesamt lässt sich sagen, dass eine Entwicklung oder Anpassung der Software ohne fundiertes Basiswissen aufwendiger und fehleranfälliger als mit den entsprechenden Grundlagen ist.
Ziel dieses Buches ist, der Leserin bzw. dem Leser die Grundlagen anschaulich und fundiert zu vermitteln. Aufgrund der Größe des Gebiets werden einige Bereiche nur gestreift, punktuell werden Themen aber in die Tiefe verfolgt.
Die Struktur des Buches mag etwas ungewohnt erscheinen. Statt eines strukturierten, aufzählenden Aufbaus wurde ein beispielorientierter erzählerischer Ansatz gewählt. Anhand von Beispielen wird die embedded Welt durchwandert und erklärt.
Ein detaillierter Index am Ende des Buches dient dem Nachschlagen einzelner Themen. Der Inhalt ist drei Teilen zugeordnet, die schichtweise aufeinander aufbauen, aber jeweils für sich separat gelesen werden können, wie Abb. 1–1 zeigt.
Abb. 1–1 Das Buch ist in drei Teile gegliedert.
Teil Ibehandelt den Aufbau eines RISC-V-Mikroprozessors, dessen Anbindung an ein Bussystem und die Grundlagen des Zugriffs auf Peripheriemodule. Grundlagen der Assemblersprache und der hardwarenahen Programmierung in C mit Memory-Mapped I/O, Speicherverwaltung und Performanz werden vermittelt.
Teil IIbeinhaltet elektrotechnische Grundlagen zum Verständnis einfacher elektronischer Schaltungen. Verschiedene Peripheriemodule zur Ein-/Ausgabe, Kommunikation, Interrupt-Behandlung sowie die Verarbeitung analoger Sensordaten werden anhand der beispielhaften Implementierung eines Pulsoximeters erläutert.
Teil IIIbettet das Pulsoximeter in den Kontext des IoT ein, indem Daten über verschiedene Internetprotokolle verschickt werden. Die Grundlagen von embedded Betriebssystemen und deren Systemprogrammierung werden anhand des Beispiels verständlich. Eine praktische Betrachtung von Bluetooth LE und der Möglichkeiten des Stromsparens rundet das Kapitel ab.
In erster Linie ist dieses Buch für den Einsatz im einführenden und fortgeschrittenen Unterricht über Embedded Systeme geplant. Es ist von großem Vorteil für das Verständnis, wenn Grundlagen der Informatik und des Programmierens in der Programmiersprache C vorhanden sind. Alternativ sind Grundlagen in einer anderen Programmiersprache sehr anzuraten. Die einzelnen Teile bieten sich an, in separaten Lehrveranstaltungen zu Rechnerarchitektur/-organisation (Teil I), Embedded Programmierung (Teil II), Betriebssystemen (Teil III) und Kommunikationssystemen/IoT (Teil III) Eingang zu finden.
In zweiter Linie richtet sich das Buch an interessierte Leser:innen mit informatischem Background. Entsprechendes Interesse vorausgesetzt profitiert diese Leserschaft vom Inhalt. Die Lesereihenfolge ist beliebig, idealerweise sequenziell vom Anfang zum Ende.
Auch Personen, die bereits im Embedded Systems-Umfeld aktiv sind, sind von diesem Buch angesprochen. Die technischen Details zu Architektur und Programmierung, die hier zusammengetragen sind, vertiefen das vorhandene Wissen. Dieser Leserschaft ist angeraten, das Buch von vorne nach hinten zu lesen und bekannte Teile zu überfliegen. Beim Überspringen könnten wertvolle Details ausgelassen werden.
Für alle Leser:innen dient das Buch als Nachschlagewerk zu den vielfältigen Technologien, die in Embedded Systemen zum Einsatz kommen.
Es ist möglich, das Buch nur zu lesen. Um ideal zu profitieren, ist anzuraten, die entsprechende embedded Hardware anzuschaffen und die Beispiele im Buch nachzuvollziehen. In diesem Sinne handelt es sich nicht um ein Lese-, sondern ein Arbeitsbuch.
Embedded HardwareAls embedded Hardware findet ein ESP32-C3-Mikrocontroller (siehe Abschnitt 2.2.1), basierend auf einem modernen RISC-V-Prozessor, Anwendung. Die Beispiele erfordern teilweise zusätzliche Komponenten wie eine Steckplatine (siehe Abschnitt 5.3.4) und Bauteile, die auch mit einem kleinen Budget zu beschaffen sind. Quellen zur Materialbeschaffung finden Sie auf der Webseite zum Buch (siehe Anhang A).
BeispielprogrammeDie Herausforderungen, die sich bei den ersten Implementierungen im embedded Umfeld ergeben, folgen typischerweise bestimmten Mustern, die sich hauptsächlich aus der Interaktion des Systems mit seiner Umgebung ergeben. Solche Muster werden im Buch zum besseren Verständnis durchgehend in Beispielprogrammen verwendet. Um diese besser nachzuvollziehen, ist der Code der Beispiele online zugänglich (siehe Anhang A) abgelegt. Dies ist vor allem beim großen Pulsoximeterbeispiel wichtig, da im Buch wesentliche Teile, aber nicht die gesamten Sourcen abgedruckt sind.
ÜbungsbeispieleJeder Teil verfügt über theoretische und praktische Übungen. Diese dienen dem Sammeln von Erfahrungen mit der embedded Plattform und dem Üben anhand typischer Problemstellungen. Ein Vergleich mit den bereitgestellten Lösungen hilft bei der Beurteilung der eigenen Ausarbeitung.
Wichtig ist dabei zu beachten, dass eine Musterlösung nicht die einzig sinnvolle Lösung darstellt. Es gibt immer viele Wege zum Ziel, die jeweils ihre eigene Begründung haben. Deshalb werden auf der Webseite zum Buch (siehe A) Kommentare zu den Musterlösungen und alternative Ansätze gesammelt und bereitgestellt.
Im Bereich der Informatik werden oft Fachbegriffe verwendet, die eine sperrige oder ungewohnte deutsche Übersetzung haben. Aus diesem Grund werden die Begriffe bei der ersten Verwendung in »französischen« Anführungszeichen eingeführt und dann direkt verwendet. Eine Vermischung wie »Embedded Systeme« statt »eingebettete Systeme« oder »embedded systems« ist eine unweigerliche Folge, die dem Autor bitte verziehen wird.
Die abgedruckten Sourcen in der Programmiersprache C wurden der Übersichtlichkeit halber auf den Platzbedarf hin optimiert. Umbrüche langer Zeilen werden mit dargestellt. Der Programmierstil ist modern und teils ungewöhnlich. So wird ein int *pX als int* pX dargestellt, um zu zeigen, dass der Pointer zum Typ gehört und nicht zum Namen. Die Benennung von Variablen und Funktionen ist modern in Camel-Case. Wer das unangebracht findet, möge bitte über diese Spitzfindigkeiten hinwegsehen und die eigenen Konventionen weiter verwenden.
Werden im Fließtext Variablen verwendet, werden diese in einer Terminal-Schriftart dargestellt. Zur einfachen Darstellung, dass es sich um eine Funktion handelt, wird diese mit runden Klammen, aber ohne Parameterliste, angegeben, beispielsweise doIt().
Abb. 1–2 Die Implementierung des Pulsoximeterspoxibildet die Basis der Teile II und III.
Weiterführendes
Einführende oder weiterführende Themen werden in separaten Kästen untergebracht. Bei Interesse können sie gelesen werden; sie sind allerdings nicht im Fließtext eingebettet.
Die Größenverhältnisse von Kleinsystemen bieten sich für eine kurze Betrachtung an. Die kleinsten Mikrocontroller sind mit Gehäuse kleiner als 2 mm x 2 mm x 0,5 mm. Der in diesem Buch verwendete ESP32-C3-Mikrocontroller hat in etwa die Leistungsfähigkeit eines Intel-Pentium-PC-Prozessors und passt mit seiner Größe 5 mm x 5 mm x 0,85 mm etwa 6½ Mal auf dessen Siliziumfläche - inklusive RAM, Wi-Fi und Bluetooth-Hardware.
Die weiteren Themen sind nicht minder spannend!
»Das Durchschnittliche gibt der Welt ihren Bestand,
das Außergewöhnliche ihren Wert.«
OSCAR WILDE
Seit Kernighan und Ritchie in ihrem Buch The C Programming Language [36] gleich zu Beginn ein Programm schrieben, das
hello, world
auf dem Bildschirm ausgab, verwenden viele Programmierbücher diesen Ansatz. Die Begründung, »der einzige Weg, eine Programmiersprache zu lernen, ist, Programme in ihr zu schreiben«, ist ja durchaus plausibel.
Da dieses Buch unter anderem den Anspruch erhebt, die Programmierung von Embedded Systemen als Fertigkeit zu erlernen, wird dieser beispielgetriebene Ansatz auch hier verfolgt. In diesem Kapitel wird ein erstes C-Programm erstellt, auf eine embedded Plattform mit einem RISC-V-basierten Mikrocontroller aufgespielt und dort gestartet. Der Debugger dient dann zur schrittweisen Programmausführung.
PC, Personal Computer: Heimcomputer oder Arbeitsplatzrechner wie Desktop, Notebook oder Tablet
Auf diese Weise werden die einzelnen Komponenten des Entwicklungsflusses verständlich. Begleitend zu dieser praktischen »Implementierung« werden die Entwicklungsumgebung und das »Embedded System«, nämlich der Rechner, auf dem das Programm ausgeführt wird, erläutert. Dieses Embedded System unterscheidet sich doch stark von einem klassischen PC mit Monitor, Tastatur, Maus und grafischer Benutzeroberfläche.
Embedded System
Unter einem embedded system (zu Deutsch: eingebettetes System) versteht man ein Computersystem, bestehend aus Hard- und Software, das in einen technischen Kontext eingebettet ist. In diesem Kontext verrichtet es Arbeiten wie Überwachung, Steuerung, Regelung und die weitere Datenverarbeitung. In modernen Systemen nimmt die Kommunikation eine wachsende Rolle ein, was sich in den technischen Modeschlagworten IoT und IIoT ([Industrial] Internet of Things: Sensoren und andere Geräte, die [im industriellen Umfeld] vernetzt sind) niederschlägt.
Embedded Systeme treten in ihren Applikationen oft so weit in den Hintergrund, dass sie für Anwender unsichtbar sind oder nicht mehr als Computer wahrgenommen werden. Beispiele sind moderne Haushaltsmaschinen, Unterhaltungsgeräte wie Uhren etc., aber auch Geräte der Kommunikationsinfrastruktur, Industrie und Fahrzeuge vom Automotive-Bereich bis zur Raumfahrt. Aufgrund dieser Unsichtbarkeit, Durchdringung und Allgegenwärtigkeit von Computersystemen stößt man im Umfeld auf die Begriffe invisible, pervasive und ubiquitous Computing.
Da solche Systeme oft mobil sind, keinen Anschluss an das Stromnetz haben, auch extremen Umweltbedingungen ausgesetzt sind und technisch in großen Stückzahlen produziert werden, liegt der Fokus auf kleinen, stromsparenden, robusten und günstigen Komponenten. Dadurch nicht vergleichbar mit üppig ausgestatteten PCs sprechen wir von ressourcenbeschränkten Systemen. Diese Beschränkung von Ressourcen wie Arbeitsspeicher, Akkukapazität, Bandbreite und Latenz der Kommunikation, Ein-/Ausgabemöglichkeiten, Antwortzeit und Echtzeitfähigkeit, Kosten etc. wirkt sich direkt auf die gesamte Hard- und Softwareentwicklung aus.
Die Hardware besteht neben Gehäuse und mechanischen Komponenten aus einer Elektronik, die diverse Schnittstellen bereitstellt. Neben LEDs, Displays, Tastern, Joysticks, Segmentanzeigen, Leistungselektronik, Sensoren für Temperatur, Druck, Helligkeit, Beschleunigung und vielem mehr enthält die Hardware als steuernde Komponente einen Mikroprozessor bzw. Mikrocontroller.
Für die Programmierung von Embedded Systemen werden verschiedene Programmiersprachen angepriesen und in der Praxis auch verwendet. Sowohl von Skriptsprachen als auch von Sprachen mit einer virtuellen Maschine wird in diesem Buch aus mehreren Gründen Abstand genommen:
Ein wesentliches Ziel dieses Buches ist es, ein grundlagenbasiertes Verständnis des Systems zu vermitteln, weshalb auf die gesamte Hardware direkt, also ohne interpretierende Zwischenschicht, zugegriffen wird. Python, Lua, Java, C# usw. fallen dadurch weg.
Das wohl stärkste Kriterium bei der Auswahl einer Programmiersprache für Embedded Systeme ist aufgrund der beschränkten Ressourcen die Performanz in Bezug auf Ausführungsgeschwindigkeit und Speichernutzung. Um den Zugang zu sämtlichen Ressourcen zu ermöglichen, spielt die Hardwarenähe ebenso eine große Rolle, was die Sprache C mit ihrem Pointer-Konzept in den Fokus rückt.
Viele Konstrukte und Paradigmen moderner Sprachen wie Objektorientierung und funktionale Programmierung spielen in den hardwarenahen Schichten eine untergeordnete Rolle. Bei der Bewältigung von Aufgaben mit hoher Komplexität, wie sie in höheren Schichten üblich sind, sind sie aber hilfreich, weshalb Sprachen mit derartigen Konzepten hier breiten Einsatz finden. Die Sprache C++ kann deshalb als durchgängige Sprache eingesetzt werden, wird aber gerade wegen der Fülle an Funktionalität und damit einhergehender Komplexität und Beherrschungsschwierigkeiten oft gemieden.
Meist fällt die Wahl als Sprache der »unteren Schichten« auf die Programmiersprache C, was sich auch dadurch ausdrückt, dass die Mikrocontrollerhersteller vorrangig C-Code in ihren Entwicklungskits, Application Notes und Treiberbibliotheken bereitstellen.
Der TIOBE Index beurteilt monatlich die Popularität von Programmiersprachen.
Aus diesen Gründen wurde auch für die Praxisbeispiele in diesem Buch die Programmiersprache C gewählt. Die Einfachheit und Klarheit der Sprache dürfen ebenfalls nicht unterschätzt werden. Die Programmiersprache C gehört laut TIOBE Index [60] zu den populärsten Programmiersprachen überhaupt. Zuletzt war C 2019 »Programming Language of the Year«.
C wurde mit dem Ziel entwickelt, eine Hochsprachenabstraktion zur Assemblersprache zu bieten. Als Resultat ist C-Code verhältnismäßig leicht in Assembler zu übersetzen, was den Aufwand der Compiler-Portierung auf eine neue Prozessorplattform gering hält. Der freie GNU C Compiler (gcc) ist auf allen gängigen Plattformen und Betriebssystemen verfügbar, und C-Programme sind damit leicht auf diese portierbar. Die Performanz des übersetzten C-Codes ergibt sich auch daraus, dass Mikroprozessorarchitekturen wie RISC-V, ARM, MIPS und weitere gemeinsam mit einem (und damit für einen) C-Compiler entwickelt werden.
Eine interessante, weil auf Performanz und Sicherheit hin entwickelte Sprache stellt Go dar. Aufgrund der derzeit geringen Verbreitung wird diese objektorientierte Sprache in diesem Buch aber nicht eingesetzt. Eine weitere Sprache mit diesem Fokus ist Rust, auf dessen Grundlage Google das embedded Betriebssystem KataOS entwickelt.
Eine weitere hardwarenahe Programmiersprache, die aber zunehmend durch Hochsprachen ersetzt wird, ist Assembler. Moderne optimierende Compiler generieren Code, der an Effizienz oft handgeschriebenes Assembly übertrifft, und das bei schnellerem Entwicklungstempo und stärkerer Sicherheit der Hochsprachen. Im Rahmen dieses Buches wird RISC-V Assembler gestreift, um die RISC-V ISA (Instruction Set Architecture) zu verstehen. Ebenso wird das Disassembly beim Debuggen verwendet, um schwer zu findende Fehler zu lokalisieren.
Damit eine Applikation entwickelt werden kann, werden verschiedene Komponenten benötigt, wie sie auch in Abb. 2–1 ersichtlich sind. Nach der folgenden Übersicht wird in diesem Abschnitt detailliert auf die einzelnen Teile eingegangen.
Abb. 2–1 Komponenten der Cross-Platform-Entwicklung
Target-SystemEinerseits wird das »Target-System«, also das Zielsystem, benötigt. Auf diesem wird die erstellte Software ausgeführt. Es ist auch möglich, per ISD (»In-System Debugging«) die Software auf der Zielplattform zu debuggen, also schrittweise auszuführen, Variablen einzusehen und vieles mehr. Für die weitere Arbeit mit diesem Buch empfiehlt sich das in Abschnitt 2.2.1 vorgeschlagene preiswerte RISC-V-Entwicklerboard.
Host-SystemWenn sich, wie in unserem Fall, das »Host-System«, auf dem die Software entwickelt wird, vom Target-System unterscheidet, spricht man von »Cross-Platform-Entwicklung«. Das Host-System ist typischerweise ein Windows-PC, auf dem die integrierte Entwicklungsumgebung (siehe Abschnitt 2.2.2) läuft. Da ein Apple Mac sich hardwaretechnisch nicht wesentlich von anderen PCs unterscheidet, ist diese Computerklasse unter dem Begriff »PC« in diesem Buch mit eingeschlossen. Mithilfe einer »Toolchain«, also einer Sammlung von Softwarewerkzeugen, wird der Sourcecode in eine Applikation übersetzt und anschließend mit einem Loader auf das Zielsystem übertragen.
Die in den Beispielen eingesetzte Software läuft auf PCs mit den Betriebssystemen Windows, Linux und macOS. Hinweise zur Installation und Benutzung sind in Anhang A.1 zu finden.
Program & Debug InterfaceDie Kommunikation zwischen Host- und Target-System wird über das Program & Debug Interface gewährleistet. In der Praxis kommen auch verschiedene Interfaces für beide Zwecke zum Einsatz, also beispielsweise serielle Kommunikation (RS-232) zum Programmieren und ein JTAG (Join Test Action Group) Interface zum Debuggen. Diese Interfaces sind üblicherweise kabelgebunden. Bei vielen Development Boards werden diese Schnittstellen zugleich mit der Stromversorgung über USB angeboten.
Die von vielen Herstellern angebotenen »Development Boards« (Entwicklungsboards) sind mit einem Mikrocontroller, verschiedener Peripherie (LEDs, Taster, Displays, Sensoren und Aktoren verschiedener Art) sowie meist einem Program & Debug Interface ausgestattet.
Das Entwicklungsboard ESP32-C3-DevKitM-1 von Espressif (siehe [14]) wird für die Beispiele in diesem Buch verwendet. Grundsätzlich kann auch ein anderes Board genutzt werden. Die Beispiele müssen in diesem Fall durch die Leserin bzw. den Leser angepasst werden.
Abb. 2–2 zeigt das Board mit den Komponenten:
ESP32-C3-MINI-1 Dieses Modul beinhaltet den RISC-V-Mikrocontroller ESP32-C3Fx4 (siehe Kapitel 3), Flash-Speicher, einen Quarz und die Antenne für Wi-Fi und Bluetooth.
Micro-USB Port Dieser Anschluss dient der Programmierung und der seriellen Datenausgabe, wofür auch die »USB-to-UART Bridge« verwendet wird. Alternativ besteht die Möglichkeit, zwei Widerstände umzulöten, um JTAG Debugging auf der USB-Schnittstelle anzubieten. In Anhang A.1 wird gezeigt, wie auf die eingebaute JTAG-Schnittstelle zugegriffen werden kann, alternativ auch ohne Notwendigkeit des Lötens.
Abb. 2–2 Espressif ESP32-C3-DevKitM-1 Development Board
5V Power On LED Diese LED leuchtet, wenn das Board per USB mit einer Spannung von 5V versorgt wird. Der »5V to 3.3V LDO« ist ein Spannungswandler, der die benötigte 3,3-V-Spannung für das Board aus der USB-Spannung generiert.
Taster Per »Reset Button« wird das System neu gestartet, wobei die Daten im RAM verloren gehen. Wird der »Boot Button« während des Neustarts gedrückt, wird der serielle Upload-Modus des Bootloaders gestartet. Andernfalls wird die Applikation gestartet. Nach dem Reset kann der Taster als Eingabemöglichkeit für die Applikation verwendet werden.
RGB LED Diese mehrfarbige LED kann in der Applikation beliebig als Benutzerschnittstelle verwendet werden.
I/O Connector Diese beiden Stiftleisten bilden die Ein- und Ausgänge des Mikrocontrollers ab. Die Beispiele des Teils IIPeripheriemodule verwenden diese Steckverbindung extensiv.
Die Ausstattung dieses Boards ist im Vergleich mit Development Boards anderer Hersteller nicht üppig. Diese Beschränkung kann aber auch von Vorteil sein: Wenn man eigene Hardware für ein embedded-Gerät aufbauen möchte und eventuell eine kleine Serie produzieren will, wird die zusätzliche Peripherie nicht verwendet und verteuert nur das Produkt. Die Vorgehensweise in diesem Fall ist, dass man eine kleine Platine mit benötigter Peripherie fertigt und das Development Board darauf ansteckt. Bei einer größeren Serie kommt dann eine Platine zum Einsatz, auf die das ESP32-C3-MINI-1-Modul und andere Komponenten des Entwicklerboards direkt aufgelötet werden.
Geliefert wird das Board in einer Konfiguration, die das Programmieren und die Ausgabe von Statusmeldungen über USB erlaubt. Ein Debuggen per JTAG ist nicht direkt möglich. Eine Spezialität des eingesetzten Mikrocontrollers ist aber ein integriertes JTAG-over-USB-Modul. Es muss also im Grunde nur ein USB-Kabel an die richtigen Pins der Stiftleisten gehängt werden, um das Debugging zu ermöglichen.
Weitere Informationen zur Verwendung des Boards, zur Installation der Software sowie zur Vorgehensweise beim Debugging sind in Anhang A.1 hinterlegt.
Um die einzelnen Komponenten und deren Zusammenspiel zu zeigen, wird statt eines Fotos wie in Abb. 2–2 üblicherweise ein schematischer Aufbau wie in Abb. 2–3 gezeigt. Bei dieser Darstellungsform, dem »Blockschaltbild«, werden Funktionsblöcke mit Rechtecken und deren Verbindungen (Signale, Leitungen) mit Pfeilen dargestellt. Elektrische und zeitliche Zusammenhänge spielen dabei eine untergeordnete Rolle, sodass das Zusammenspiel der Komponenten im Vordergrund steht und intuitiv erfasst werden kann. Die Pfeile geben dabei die logische Richtung des (Signal-)Flusses an.
Abb. 2–3 Schematischer Aufbau des Espressif ESP32-C3-DevKitM-1
Weitere Symbole, wie in der Abbildung das USB-Symbol links und das Antennensymbol oben, können zur weiteren Veranschaulichung verwendet werden. Im schematischen Aufbau ist so beispielsweise ersichtlich, dass
die beiden Taster unten ihren Status an das ESP32-Modul senden,
die RGB LED vom Modul gesteuert wird,
das Modul über die Leitungen TX und RX per USB-UART Bridge an USB bidirektional angeschlossen ist, also Daten senden und empfangen kann,
zwei Stecker (»x2«) angeschlossen sind, über die Daten ein- und ausgegeben werden können,
der LDO aus der USB-Spannung die 3,3V für das Modul erzeugt und
eine Antenne angeschlossen ist.
Derartige Blockschaltbilder werden in der Praxis vielfach verwendet. So finden sie auch in den Datenblättern und Reference Manuals der verschiedenen Hersteller Verwendung.
Auf dem Host-System, also dem PC, wird verschiedene Software für die Programmentwicklung benötigt. Grundsätzlich stellt sich die Wahl, ob kostenfreie oder zahlungspflichtige Software verwendet werden soll. In diesem Buch wird, wie in vielen Projekten der Wirtschaft, robuste und ausgereifte freie Software verwendet, um den Einstieg in die Entwicklung zu erleichtern.
In der Praxis kann es auch aus mehreren Gründen nötig werden, zahlungspflichtige Software einzusetzen. Teilweise sind Bibliotheken oder Support nicht frei verfügbar, teilweise möchten sich die Entwickler auch gegen die Verwendung fehlerhafter Tools absichern: Nur wenn der Hersteller bekannt ist und für die Software haftet, kann im Schadensfall ein Regress erfolgreich abgewickelt werden.
Ein grundsätzlicher, wesentlicher Aspekt bei Entwicklung und Vertrieb von Software ist die Haftung im Fehlerfall. Mögen Fehler in kaufmännischer Software finanzielle Schäden bewirken, die auch finanziell abgegolten werden können, besteht bei Geräten mit Einfluss auf die Umwelt das Problem, dass Sach-, Umwelt- oder auch Personenschäden auftreten können. Da diese teils strafrechtlichen Konsequenzen nicht durch Versicherungen abgedeckt werden können, muss hier zu besonderer Vorsicht und Einhaltung der bestehenden Richtlinien geraten werden.
Unter einer »Development Toolchain« wird eine Sammlung von Werkzeugen verstanden, die bei der Implementierung von Software eingesetzt wird. Unter der Implementierung wird der Teil der Softwareentwicklung verstanden, der sich mit der Programmierung, Ausführung und der Fehlerlokalisierung beschäftigt. Essenzielle begleitende und organisatorische Maßnahmen wie Analyse, Design, Spezifikation, Testen, Dokumentation, Zertifizierung usw. sind nicht Teil der Implementierung.
Abb. 2–4 Cross-Platform Development Toolchain
Abb. 2–4 zeigt die wesentlichen Werkzeuge, die beim Kompilieren und Aufspielen der Applikation zum Einsatz kommen. Es befinden sich noch wesentlich mehr Werkzeuge in der Toolchain, die aber in der Abbildung nicht gezeigt sind.
Die auf dem Host-System befindlichen Quellcodedateien (in den Sprachen C, C++ geschrieben) werden vom Compiler in die Assemblersprache des Target-Systems übersetzt. Dabei handelt es sich um eine Sprache, die die Befehle (»Instructions«) der Maschinensprache in menschenlesbarer Form (sogenannte »Mnemonics«) abbildet. Ein Assembler übernimmt dann die Transformation der Mnemonics in Instructions und erzeugt Objektdateien. Die entstandenen Assemblerdateien sind temporär und werden wieder gelöscht. Der Linker nimmt diese und weitere Objektdateien (z.B. aus Bibliotheken) und ordnet sie hintereinander im Speicher an. Referenzen (»Links«) von einer Datei in eine andere werden dabei aufgelöst.
Das Ergebnis dieses sogenannten »Build-Prozesses« ist die Applikation als Binärdatei, typischerweise im ELF(Executable and Linking)-Format mit Debug-Informationen oder im Intel HEX(Hexadecimal Object File)-Format ohne weitere Informationen. Da diese Applikation auf dem Host-System erzeugt und gespeichert wird, aber auf dem Target-System ausgeführt, also eine Systemgrenze überquert wird, spricht man hier von »Cross-Platform Development«.
Eine wichtige begriffliche Unterscheidung liegt zwischen »statisch« (zur Compile-Zeit) und »dynamisch« (zur Laufzeit). Die Laufzeit eines Programms beginnt mit dem Laden des Programms in den Speicher. Somit sind bereits das Laden, das Anlegen der globalen Variablen im RAM, die Ausführung, die Reservierung von Speicherplatz für lokale Variablen auf dem Stack usw. dynamisch. Das Schreiben des C-Codes, das Kompilieren und Linken (»Binden«), das Schreiben des Programmspeichers usw. sind hingegen statisch.
Es gibt auch Systeme ohne Bootloader, die auf anderen Wegen (z.B. per JTAG) programmiert werden.
Um die Systemgrenze physisch zu überwinden, wird die Applikation über einen Loader auf das Target-System übertragen. Am Zielsystem ist hierfür eine minimale Startsoftware untergebracht, der sogenannte Bootloader, der die Applikation per serieller Verbindung übernimmt und permanent speichert. Der Bootloader lässt sich in ausgelieferten Produkten deaktivieren, sodass eine nachträgliche Änderung der Software nicht mehr möglich ist.
Für die Beispiele in diesem Buch wird eine Anpassung der GNU Toolchain für RISC-V in Verbindung mit weiteren Tools unter dem Namen ESP-IDF (Espressif IoT Development Framework) verwendet. Informationen zur Installation finden Sie in Anhang A.1.
Ursprünglich erfolgte die Bedienung der Toolchain über eingegebene Kommandos in der Konsole. Mit einem separaten Editor wurde der Quellcode geändert, mit einem Terminalprogramm wurden die Programmausgaben angezeigt. Moderner ist die Verwendung einer IDE (Integrated Development Environment), die Editoren und Toolchain unter einer Oberfläche miteinander vereint und automatisiert verknüpft.
Für den ESP32-C3 werden die weit verbreiteten IDEs Eclipse und Visual Studio Code mit entsprechenden Erweiterungen (Plug-ins) unterstützt. Für die Beispiele in diesem Buch wird Eclipse aufgrund der weiten Verbreitung im embedded Umfeld verwendet.
Abb. 2–5 zeigt die ursprünglich für Java entwickelte, leicht erweiterbare IDE Eclipse während des Debuggings. Das Mittelfenster zeigt den Quellcode in automatischer Einfärbung, während die Programmausführung gerade in Zeile 128 angehalten ist. Rechts ist das Variablenfenster, das die aktuellen Werte der lokalen Variablen anzeigt, zu sehen. Die tatsächliche Ausführung findet dabei direkt auf dem angeschlossenen Mikrocontroller statt (ISD, In-System Debugging).
Weitere Informationen zur Installation und Verwendung der IDE Eclipse und auch der Alternative Visual Studio Code finden Sie in Anhang A.1.
Abb. 2–5 Eclipse IDE beim Debugging
Üblicherweise werden bei der Softwareentwicklung weitere Tools, wie Terminalprogramme, verwendet. Deren typische Vertreter, wie »Putty« oder »Tera Term«, eignen sich, um mit Geräten zu kommunizieren. Bei der eingebetteten Entwicklung können so textuelle Ein- und Ausgaben gemacht werden.
Weitere Software kann den Softwareentwicklungsprozess überwachen beziehungsweise leiten. In der Softwareentwicklung zählen ja nicht nur die Programmierfertigkeit und technische Realisierung. Vielmehr ist wichtig, dass die Spezifikation des Produktes schlüssig ist und die Implementierung dieser Spezifikation entspricht. Weitestgehende Fehlerfreiheit, Wartbarkeit und gute Dokumentation für Entwickler:innen und Anwender:innen sind in der Praxis Pflicht. Einen guten Einblick in die Thematik liefert das Buch »Software Engineering« [56].
Nach der Installation der Espressif Toolchain und IDE kann das erste Projekt hello_world aufgesetzt werden (siehe Anhang A.1).
Das Original
Die »Urfassung« des Hello-World-Programmes von Kernighan und Richie [36] hat eine überraschende Signatur der main()-Funktion:
#include <stdio.h>
main()
{
printf("hello, world");
}
Seit ANSI C99 muss der Rückgabedatentyp explizit angegeben werden, anstatt implizit int zu verwenden. Mit der void-Parameterliste liest sie sich
intmain(void).
Wird die Funktion von einem Betriebssystem aufgerufen, werden die Eingabeparameter auch mit angegeben, also
intmain (intargc, char* argv[]).
Der Quellcode des Beispiels aus dem ESP-IDF (Extrakt in Listing 2.1) weicht von der »Urfassung« im Kasten »Das Original« in einem wichtigen Punkt ab: Die main()-Funktion heißt app_main, mit leerer Parameterliste und Rückgabe.
#include <stdio.h>
/* [...] other */
voidapp_main(void) {
printf("Hello world!\n");
/* [...] print system infos and reset system after 10s */
}
Listing 2.1 Extrakt deshello_world-Beispiels aus dem Espressif IDF
Dies liegt daran, dass beim ESP-IDF das embedded Betriebssystem FreeRTOS verwendet wird. Aufgrund der wachsenden Ressourcen, wie RAM, Flash, Taktfrequenz und weiteren, sowie der damit einhergehenden wachsenden Applikationen und Softwarekomplexität gewinnen embedded Betriebssysteme zunehmend an Bedeutung. Diese ermöglichen die Ausführung mehrerer Tasks gleichzeitig, meist unter Einhaltung strikter zeitlicher Schranken für die Abarbeitung der Algorithmen.
Die Applikation wird im ESP-IDF als solch ein Task gestartet, weshalb der main-Einstiegspunkt die Initialisierung von Hardware und Betriebssystem übernimmt. Die Funktion app_main() ist die Hauptfunktion des main-Tasks. Mit embedded Betriebssystemen und deren Mechanismen befasst sich Abschnitt 9.3 in Teil III dieses Buches ausführlich.
ProgrammausführungNach dem erfolgreich durchgeführten Build wird die Applikation zur Programmausführung erst in den persistenten Speicher (Flash) des eingebetteten Systems kopiert. Das eingesetzte ESP32-C3-DevKitM-1 Board hat einerseits eine USB-Schnittstelle für den seriellen Anschluss, andererseits aber auch den Zugang zu einem JTAG-Interface über USB an dem Erweiterungsstecker. Der Upload der Applikation kann entweder über die serielle Schnittstelle oder über das JTAG-Interface erfolgen. In der Eclipse IDE wird der Build- und Uploadprozess über den »Launch«-Button, wie in Abb. 2–6 orange eingekreist, angestoßen. Die »Console« zeigt dabei den Vorgang und Erfolg des Uploads.
Abb. 2–6 Upload einer Applikation in Eclipse
Während des anschließenden Neustarts werden vom ESP-IDF Bootloader Informationen über das Embedded System auf der seriellen Schnittstelle ausgegeben, weshalb der Text Hello world! erst weiter unten im »Terminal« erscheint. In der Abbildung wurde die Konsole im Arbeitsbereich links und das Terminal unten angeordnet. Die einzelnen »Views« können in Eclipse beliebig verschoben werden. Weitere Informationen zur Verwendung der IDEs sind in Anhang A.1 zu finden.
DebuggingWird in der printf-Zeile ein Breakpoint gesetzt und der Debugger über den »Debug«-Button gestartet, wird die Initialisierung ausgeführt und die Programmausführung in der Zeile mit dem Breakpoint angehalten, wie in Abb. 2–7. Es besteht nun die Möglichkeit, Inhalte von Variablen, Registern, Speicher und mehr einzusehen und sogar direkt auf dem Target-System zu verändern.
Abb. 2–7 Eclipse ist am Breakpoint angehalten.
In den folgenden Kapiteln dient der Debugger dazu, mehr über den eingesetzten Mikrocontroller sowie die Codeausführung zu erfahren. Nähere Informationen zum Debugging sind auf der Webseite zum Buch (siehe Anhang A.1) zu finden.
»Die Gefahr, dass der Computer so wird wie der Mensch, ist nicht so groß wie die Gefahr, dass der Mensch so wird wie der Computer.«
KONRAD ZUSE
In diesem Kapitel wird die Arbeitsweise des Mikroprozessors beziehungsweise der CPU (»Central Processing Unit«) erklärt. Ausgehend von einem Programm zur Berechnung der Summe der ersten n natürlichen Zahlen wird die RISC-V-Architektur und deren Arbeitsweise und Assemblersprache erläutert. Eine Erläuterung der Möglichkeiten zur Messung von Taktzyklen und ausgeführten Instruktionen direkt im System rundet die Betrachtung der Systemperformanz ab.
Die im Folgenden vermittelten Kenntnisse sind eine Grundlage für die praktische Anwendung sowie Ausgangspunkt für ein Verständnis der detaillierteren Fachliteratur.
Ein Mikroprozessor bzw. eine CPU ist ein Rechenwerk, das einen Algorithmus ausführen kann.
Ein Mikroprozessor ist ein auf einem integrierten Schaltkreis (IC, »Integrated Circuit«) untergebrachtes Rechenwerk, das eine Liste von Befehlen abarbeiten kann und damit einen Algorithmus bzw. Prozess ausführt. In einem Computer wird der Mikroprozessor als CPU, »Central Processing Unit«, eingesetzt. Ähnlich dem Trommeln für den gleichzeitigen Ruderschlag auf einer Galeere dient ein Systemtakt der Steuerung und Synchronisation des Datenflusses. Ein idealisierter Prozessor schafft es, pro Takt eine Instruktion abzuarbeiten.
Bei der Softwareentwicklung auf hohem abstrakten Niveau ist die Kenntnis der Funktionsweise eines Mikroprozessors nicht dringend erforderlich, da Betriebssysteme und Hochsprachen wie Java mit virtuellen Maschinen die Eigenheiten der Hardware weg abstrahieren. In der embedded Programmierung kann es aber von Vorteil sein, dieses Wissen zu haben und beispielsweise bei der Optimierung zeit- und speicherplatzkritischer Programmstücke einzusetzen. Auch bei der Fehlersuche und -behebung ist es von Vorteil, wenn man sich anhand des Disassembly, also des kompilierten Codes, zurechtfindet und den Fehler damit feingranularer orten kann als im Debugger der Hochsprache.
Wie einst dem jungen Mathematiker Gauß, dessen Klasse als Beschäftigung aufgetragen wurde, die Zahlen von 1 bis 100 zusammenzuzählen sind an dieser Stelle auch Sie, werte Leserin, werter Leser, gebeten, ein Programm zu schreiben, das die Zahlen von 1 bis 100 zusammenzählt. Dieses Beispiel stellt dann die Grundlage der weiteren Betrachtungen in diesem Kapitel dar. Nach Bewältigung dieser kleinen Programmieraufgabe sollte der Code dem von Listing 3.1 ähneln. Der Einsatz der kopfgesteuerten while-Schleife macht die folgende Analyse verständlicher als die in C meist verwendete for-Schleife.
Listing 3.1 Das Beispiel berechnet die Summe der Werte 1 bis 100 in einer Schleife.
Die Verwendung der inttypes.h Datentypen ist bei der embedded Programmierung für die Portabilität von besonderer Bedeutung (siehe dazu auch den Kasten »Arithmetische Datentypen in C«).
Arithmetische Datentypen in C
Der C-Standard definiert die arithmetischen ganzzahligen (»Integer«)-Datentypen char und int, die jeweils signed oder unsigned vorliegen können. Ebenso kann die Breite (der Wertebereich) von int durch Voranstellen von short, long und long long verändert werden.
Allerdings ist die tatsächlich verwendete Anzahl an Bits für den jeweiligen Datentyp architekturabhängig. int ist auf 8- und 16-Bit-Systemen typischerweise 16 Bit breit, auf 32-Bit-Systemen 32 Bit breit und auf 64-Bit-Systemen 32 oder 64 Bit breit. Dies verkompliziert die portable Programmerstellung für verschiedene Systeme.
Abhilfe schafft hier das Modul inttypes.h bzw. stdint.h. Hier wird die Breite im Datentyp mit angegeben, also beispielsweise int16_t für eine 16 Bit breite vorzeichenbehaftete Zahl. Ein vorangestelltes u definiert eine vorzeichenlose Zahl, also in diesem Beispiel uint16_t. Das Modul bietet außerdem noch die Möglichkeit, schnelle Zahlen (z.B. uint_fast16_t) und Zahlen mit Mindestgröße (z.B. uint_least16_t) sowie Pointer (z.B. uintptr_t) und den größten Datentyp (uintmax_t) portabel zu definieren. Die Wertebereiche dieser Zahlen stehen auch über Makros wie (UINTMAX_MAX oder auch INT_FAST16_MIN und INT_FAST16_MAX) zur Absicherung des Codes zur Verfügung.
Durch Implementierung des Standards IEEE754 sind die Fließkommazahlen float und double auf den Systemen portabel. long double hat aber verschiedene Implementierungen und liefert deshalb durch unterschiedliche interne Darstellungen und Rundungen nicht auf allen Systemen dieselben Resultate. Grundsätzlich ist aber von der Verwendung von Fließkommazahlen wo möglich abzusehen, da diese Zahlen einerseits durch die Rundungen mathematisch schwer beherrschbar (das Feld der numerischen Mathematik beschäftigt sich mit diesen Zahlen, bei denen Assoziativgesetz und Distributivgesetz nicht allgemein gelten) und andererseits aufwendiger in der Berechnung sind.
Nachdem der Build-Prozess durchlaufen ist, die Applikation auf das Target aufgespielt und der Debugger aufgespielt wurde, befindet man sich in der Debug-Ansicht der IDE (diese Vorgehensweise ist in Kapitel 2 beschrieben). In Abb. 3–1 sind die Fenster für den Sourcecode und das Disassembly nebeneinander angezeigt. Der Code wurde per Single-Stepping bis zum Ende der 4. Schleifenrunde durchlaufen.
Bei der Fehlersuche kann es hilfreich sein, das Disassembly des Codes anzuzeigen. Hierbei handelt es sich um eine Rückübersetzung des beim Build-Prozess erzeugten Maschinencodes in die Assemblersprache. Durch die Verwendung von Debuginformationen ist es möglich, den ursprünglichen Quelltext und das Assembly gemeinsam anzuzeigen.
Abb. 3–1 Applikation sum_up_n: Debug-Ansichten Sourcecode und Disassembly
Beispielsweise ist erkennbar, dass das C-Statement i += 1; dem Assemblerstatement addi a5,a5,1 entspricht. Der darauffolgende Sprung j 0x420052fa <app_main+20> gehört aber zur while-Schleife, was nicht direkt ersichtlich ist.
Der generierte Code ist von der Compilerversion und der eingestellten Optimierung abhängig. Bitte dies beim Nachvollziehen dieses Kapitels zu beachten.
Im C-Code sind in der linken Spalte die Zeilennummern dargestellt. In den Assemblerbefehlen (auch »Instruktionen«) ist jeweils die Adresse im Speicher, an der der Befehl liegt, in Hexadezimaldarstellung gefolgt von einem Doppelpunkt angegeben. Eine Zeile ohne Nummer, wie app_main:, ist eine Sprungmarke (»Label«), in diesem Fall der Name der Funktion.
In Assembler werden die Befehle einzeln in Zeilen untereinander geschrieben. Am linken Rand der Zeile stehen Sprungmarken mit abschließendem Doppelpunkt, wie die Marke app_main:, die den Einsprungspunkt für die Applikation deklariert.
Ein Mnemonic ist ein Kürzel für einen Befehl, wielifürload immediate [value].
Assemblerbefehle müssen eingerückt werden und können einen abschließenden, durch ein Semikolon eingeleiteten Kommentar enthalten. Ein Befehl besteht aus einem »Mnemonic« und darauf folgenden Parametern. Die Parameter können Register, direkte Werte, Sprungmarken und mehr sein.
Wenn ein Befehl schreibend auf ein Register zugreift, ist das Ziel das erste nach dem Befehl angegebene Register. add a2, a3, a4 liest beispielsweise die Inhalte der beiden Register a3 und a4 aus, addiert diese und schreibt das Ergebnis in Register a2.
Im weiteren Verlauf des Kapitels wird die RISC-V-Assemblersprache verwendet, um die Arbeitsweise des Prozessors im Detail zu besprechen. Vor einem tieferen Einstieg in die Assemblersprache sollten in der Entwicklungsumgebung noch die Fenster »Variables« und »Registers« eingeblendet werden. Abb. 3–2 zeigt die Fensterinhalte für die aktuelle Programmposition.
Abb. 3–2 Applikation sum_up_n: Debug-Ansichten Variables und Registers
Die Werte der angezeigten Variablen sind aktuell, gelb hinterlegt sind die Werte, die sich im letzten Schritt geändert haben. In der Abbildung ist das die Variable sum durch das vorangegangene Statement sum += i;. Am Ende des 4. Schleifendurchlaufs hat sum den Wert 10 (= 1 + 2 + 3 + 4) und i den Wert 4. Die nächste auszuführende Anweisung ist der Inkrement von i in Zeile 14.
Die Registerbank ist der kleinste und schnellste Datenspeicher eines Computers.
Im Registers-Fenster ist zu sehen, dass a2 in der letzten Anweisung geändert wurde und denselben Wert wie sum hat. Ebenso fällt während des Debuggens auf, dass a4 upperNumber und a5 i zu entsprechen scheint. Tatsächlich handelt es sich bei den Registern (beziehungsweise der Registerbank oder »Register File«) um den innersten und schnellsten Datenspeicher der CPU, der zur Ablage dieser lokalen Variablen verwendet wurde.
Designprinzip KISS: Keep It Simple, Stupid
Ein Mikroprozessor hat eine stark beschränkte Zahl von Registern, die als Operanden in Befehlen verwendet werden können. Ein ARM-Prozessor hat beispielsweise 16 Integer Register, ein RISC-V verhältnismäßig »üppige« 32 Integer Register. Prozessoren mit Fließkommaeinheiten haben zusätzliche Fließkommaregister. Im weiteren Text wird Integer Register mit Register gleichgesetzt. Die Breite der Register entspricht üblicherweise der Breite der internen Busse und wird auch als »Architekturbreite« bezeichnet. Wenn die Register beliebig ausgelesen, beschrieben und durch die Befehle verarbeitet werden können, handelt es sich um eine »General Purpose Register Machine« (GPRM). Die RISC-Prozessoren der ARM- und RISC-V-Familien sind GPRM.
Die RISC-Philosophie
RISC, Reduced Instruction Set Computer wurde von D. Patterson und C. Séquin im Gegensatz zu CISC, Complex Instruction Set Computer definiert. Diese nach dem KISS-Prinzip entwickelte Architektur bietet wenige hochoptimierte Maschinenbefehle, was unter anderem die Größe der benötigten Siliziumfläche verringert und den Befehlsdurchsatz erhöht. Der Befehlssatz wird auf optimierende Compiler von Hochsprachen anstatt die direkte Assemblerprogrammierung ausgelegt. Die C-Compiler werden gemeinsam mit dem Befehlssatz entwickelt, was die hohe Performanz dieser Programmiersprache erklärt.
Die Erweiterung von RISC-Befehlssätzen durch Befehle, die viele Daten bearbeiten (SIMD, Single Instruction, Multiple Data), weichen das RISC-Prinzip in modernen Prozessoren auf, womit »RISC« mehr und mehr zu einer Worthülse verkommt. Diesem Trend entgegenwirkend hat die RISC-V-Architektur eine Erweiterung mit Vektor- statt SIMD-Instruktionen, was die Größe des Befehlssatzes wiederum gegenüber der Alternative reduziert.
Die 32 Register der RISC-V-CPUs sind von x0 bis x31 durchnummeriert. Register mit speziellen Verwendungen wie der Stack Pointer (sp) oder das Link Register (lr), das die Adresse des letzten Aufrufs enthält und automatisch vom Prozessor verwendet wird, sind nicht vorhanden. Dennoch wird die Nutzung im ABI (siehe Abschnitt 3.3.2), das das Zusammenspiel zwischen Programmmodulen regelt, vorgeschlagen. Tabelle 3–1 listet die Register und ihre per ABI zugeordnete alternative Benennung und Verwendung auf. Der C-Compiler arbeitet mit diesen Registerbezeichnungen und verwendet die Register gemäß der Spezifikation. Im Disassembly und im Fenster »Registers« werden ebenso die Bezeichnungen des ABI verwendet.
Sehr eigentümlich, aber auch praktisch ist das fest mit dem Wert 0 verdrahtete Register zero: Wenn es ausgelesen wird, liefert es immer den Wert 0 zurück, und ein Schreiben des Registers wird ignoriert. Dies hat unter anderem den Vorteil, dass die Konstante 0 nicht immer wieder separat geladen werden muss. Eine weitere Verwendung in Pseudoassemblerbefehlen wird in diesem Kapitel noch erläutert.
Bei ARM-Prozessoren sind die Register r13 als sp und r14 als lr fix reserviert und werden intern verwendet. Bei einem Funktionsaufruf wird die Rücksprungadresse automatisch gesichert, bei Abhandlung eines Interrupts, also einer asynchronen Unterbrechung des Programmflusses (siehe Abschnitt 6.1), wird der aktuelle Prozessorstatus automatisch auf dem Stack gesichert. Da diese Automatismen in der RISC-V-Architektur fehlen, werden sie (üblicherweise vom Compiler) in Code ausprogrammiert. Dies vergrößert zwar den Code etwas, macht die Architektur aber simpler und damit günstiger.
Ein weiteres reserviertes ARM-Register ist r15, in dem der Program Counter (pc) gespeichert wird. Dieses Register beinhaltet die Adresse des nächsten auszuführenden Befehls. In der RISC-V-Architektur ist der pc wiederum aus Gründen eines einfacheren Prozessoraufbaus nicht in der Registerbank enthalten, wohl aber im »Registersatz«. Hier finden sich auch die Register wieder, die nicht direkt durch Programme angesprochen werden können und nur intern im Prozessor verwendet werden. Sprünge werden mit eigenen Befehlen, die den pc ändern, realisiert.
Für eine Betrachtung des schematischen Aufbaus eines RISC-V-Prozessors findet das in Abb. 3–3 dargestellte Blockschaltbildsymbol für die Registerbank Verwendung. In dieser Darstellung bedeuten schwarze Pfeile einen Datenfluss in der angegebenen Richtung, die hellblau eingefärbten Linien ein Steuersignal. Ein Schrägstrich mit zugefügter Zahl durch ein Signal gibt die Anzahl an parallelen Leitungen, die für dieses Signal verwendet werden (und somit gleichzeitig übertragene Bits), an.
Abb. 3–3 Blockschaltbildsymbol der Registerbank
Um ein Register zu schreiben, wird der Index des zu schreibenden Registers über »Write register« ausgewählt und die Daten werden über »Write data« unter gleichzeitigem Setzen des Signals »Reg-Write« übergeben. Aus der Registerbank können gleichzeitig zwei verschiedene Register, deren Index über »Read register 1/2« gesetzt wird, ausgelesen werden. Die Daten stehen dann an »Read data 1/2« zur Verfügung.
Um beispielsweise Register 10 auf einen Wert zu setzen, muss »Write register« mit dem Wert 10 und »Write data« mit dem Wert belegt und »RegWrite« gesetzt werden. Im nächsten Takt wird der Wert dann übernommen.
Beispielhafte Assemblerbefehle aus dem Disassembly, die lesend und/oder schreibend auf Register zugreifen, sind:
li a4,100
Lade die Konstante 100 in Register a4.
add a2,a2,a5
Addiere die Inhalte von Register a2 und Register a5 und schreibe das Ergebnis in Register a2.
Neben dem innersten Speicher, der sich in den Registern befindet, wird in einem Rechner ein Rechenwerk benötigt. Dieses, die »Arithmetic Logic Unit (ALU)«, ist schematisch in Abb. 3–4 dargestellt.
Abb. 3–4 Blockschaltbildsymbol der Arithmetic Logic Unit (ALU)
Die ALU beherrscht eine Auswahl arithmetischer Operationen auf Ganzzahlen. Das sind hauptsächlich die Grundrechnungsarten und bitweise logische Operationen wie AND, OR, XOR, NOT sowie Bitmanipulationen wie Shifts, Rotationen, Setzen und Löschen einzelner Bits (siehe dazu auch Abschnitt 4.4).
Bits, Bytes und Vorsätze für Maßeinheiten