Teil 2
Im zweiten Teil werden wir versuchen, das erste eigene Programm zu schreiben. Manches lässt sich einfacher Umsetzen, wenn man die Grundlagen von C bzw. C++ schon einmal gehört hat. Daher gehen wir an den Anfang der Bits und Bytes. Daraus ergibt sich dann später der eine oder andere Aha-Moment.
Grundlagen in C / C++
Die Quelldateien, die in der Arduino IDE verwendet werden, sind normalerweise in C++ programmiert. Da vieles im Hintergrund von dem Programm automatisch hinzu kompiliert wird, ähnelt es eher der Sprache C. Dazu folgen nun einige Dinge, die man aus der Informatik wissen sollte.
Nach dem Ende des Kompiliervorgangs erschien im Fenster der IDE im unteren Teil der Hinweis zum verfügbaren und verwendeten Speicher des Mikrocontrollers. Es wird unterschieden zwischen Programmspeicher und dynamischem Speicher. Gefüllt wird er durch unser Programm. Nutzt man sehr umfangreiche Bibliotheken, ist der Speicher schnell voll, was zu Fehlverhalten des Arduinos führt.
Man sollte also vorher bereits abschätzen, wie umfangreich das Projekt wird. Reserviert wird dieser Speicher zum Beispiel durch Variablen. Diese muss man dem Compiler bekannt machen. Man nennt das „Deklarieren“. Das ist wichtig, da dadurch Programmspeicher reserviert und adressiert wird. So kann der Prozessor des Arduinos später immer wieder darauf zugreifen.
Da Menschen eher Wörter, als Adressen verstehen, nutzen wir Namen für Variablen und Funktionen. Diese können wir frei wählen, müssen dabei aber streng auf Schreibweisen achten und wir sollten eindeutige Namen vergeben. Eine Variable für ein Ergebnis sollte nicht „Variable“ heißen, sondern dann auch „Ergebnis“.
Tipp: Ein Compiler übersetzt unseren Programmcode in Maschinensprache, die der Prozessor versteht.
Der Speicher besteht aus Bits. Diese werden zusammengefasst in Bytes. Ein Byte besteht aus 8 Bits, wobei jedes Bit den Zustand 0 oder 1 annehmen kann. Das ergibt nun verschiedene Kombinationen. 2^8 also 2 x 2 x 2 x 2 x 2 x 2 x 2 x 2 um genau zu sein. Die Zahl 2 wird in jedem Schritt mit 2 multipliziert. Das ergibt die üblichen Schritte der Zahlen in der Informatik: 1, 2, 4, 8, 16, 32, 64, 128, 256 usw.
Man kann mit 8 Bits also 256 verschiedene Werte annehmen. In der Informatik wird immer bei 0 begonnen, also 0 bis 255. Das ist zu Beginn etwas verwirrend. Liest man die Bits in einer Reihe (meist von rechts nach links), entstehen binäre Codes (hier 8 Bit, binär steht für zwei):
0 0 0 0 0 0 0 0 = 0 dez
0 0 0 0 0 0 0 1 = 1 dez
0 0 0 0 0 0 1 0 = 2 dez
0 0 0 0 0 0 1 1 = 3 dez
…
0 0 0 0 0 1 0 0 = 4 dez
…
0 0 0 0 1 0 0 0 = 8 dez
…
0 0 0 1 0 0 0 0 = 16 dez
…
0 0 1 0 0 0 0 0 = 32 dez
…
0 1 0 0 0 0 0 0 = 64 dez
…
1 0 0 0 0 0 0 0 = 128 dez
…
1 1 1 1 1 1 1 1 = 255 dez
Ausführliche Tabellen dazu findet man ausreichend im Internet. Die Liste kann sehr lang werden. Um das Lesen von Binärzahlen zu verdeutlichen, wurden hier die weiter oben genannten Zahlenschritte dargestellt. Für jede dieser Zahlen hat immer nur eine Stelle den Zustand 1, die anderen den Zustand 0. Betrachtet man die entsprechenden Dezimalzahlen, erkennt man, dass es sich bei jedem Schritt um eine Verdoppelung handelt. Also eine Multiplikation mit 2. Die 1 „wandert“ nach links.
Tipp: dieses Wandern in eine Richtung nennt man „shift“ und funktioniert auch mit allen anderen Werten. Es gibt in der Programmiersprache die shift-Operatoren „<<“ und „>>“ für Links- und Rechtsshift.
Tipp: shift ist eine schnelle Variante der Multiplikation mit 2
Möchte man nun herausfinden, um welchen dezimalen Zahlenwert es sich handelt, muss man nur von rechts nach links alle entsprechenden Werte zusammenrechnen, die auf 1 gesetzt sind. In Tabellenform lässt sich das leichter darstellen:
Dezimal | 255 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | |
---|---|---|---|---|---|---|---|---|---|---|
Binär | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | Von rechts nach links: 1 + 0 + 4 + 8 = 13 |
Die Zahl 13 ist z.B. in binär 0 0 0 0 1 1 0 1. Von rechts nach links addieren wir also alle Zahlenwerte, die in der Binär-Zeile den Zustand 1 besitzen, zusammen. Alle anderen Werte belassen wir bei 0. Normalerweise werden die ungenutzten 0-Werte auf der linken Seite weggelassen. Also 1 1 0 1.
Tipp: Man kann erkennen, dass die Binäre Zeile wie eine Schalterleiste funktioniert. Das wird für Mikrocontroller oft genutzt und nennt sich „Bit-Maske“.
Tipp: Wenn die kleinste Stelle (hier rechts außen) den binären Zustand 1 einnimmt (also eingeschaltet ist), handelt es sich um eine ungerade Dezimalzahl.
Warum der Exkurs in die Welt der Binärzahlen? Nun die Menge der Bits bzw. Bytes bestimmt, wie voll der Speicher des Arduinos sein wird. Beginnt man mit einem Programm, kann man diese Ressource einteilen. Dafür werden Datentypen eingesetzt, für die unterschiedlich viele Bits reserviert werden.
Mit Blick auf die maximale Speichergröße eines Bytes könnte man außerdem gleich das erste Problem erkennen. Möchte man zwei Bytes addieren und man hat nur 256 Werte für das Ergebnis zur Verfügung, dann könnte das zu falschen Resultaten führen. 200 + 200 ergibt 400 und kann nicht durch ein Byte dargestellt werden.
Es kommt zu einem „Überlauf“. Werte über dem Maximum sind „nicht berechenbar“. Der Prozessor wird dann zur Laufzeit (so wird der laufende Betrieb genannt) wahrscheinlich bis zur 255 „zählen“ und anschließend wieder bei 0 beginnen. Das muss aber nicht immer so sein. Man kann es sich wie eine analoge Uhr vorstellen, die statt 1 bis 12 Uhr von 0 bis 255 Bit darstellt. Das errechnete Ergebnis wäre nicht 400, sondern 144.
Man kann das mit dem Arduino gut testen. Wir erstellen einen neuen Sketch. Dort deklarieren wir uns drei Variablen vom Typ Byte. Wir nennen sie zahl1, zahl2 und zahl3. Die ersten beiden Variablen definieren wir jeweils mit dem Wert 200. Die dritte Zahl wird später unser Ergebnis sein. In den Sprachen C und C++ erkennt man die Deklaration und Definition einer Variablen an dem vorangestellten Datentypen (weitere Typen werden später noch vorgestellt), dem Variablennamen, dem Zuweisungsoperator und dem Wert, mit dem wir die Variable definieren.
Tipp: An das Ende einer Codezeile muss immer das Semikolon gestellt werden. Eine häufige Fehlerquelle.
Tipp: Der Name einer Variablen kann immer frei gewählt werden. Man sollte jedoch erkennen, worum es sich hierbei handelt. Im restlichen Quellcode muss dann der exakte Name wiederverwendet werden, möchte man auf den Wert der Variablen zugreifen.
Tipp: Deklaration bedeutet immer erzeugen und reservieren. Definition dagegen bedeutet einen Wert dafür festzulegen.
Für größere Programme ist es wichtig, zu Beginn Speicher zu reservieren. Der Wert unserer Ergebnisvariable „zahl3“ ist noch nicht bekannt, da wir noch kein Ergebnis haben. Also ist der Wert für unser Verständnis 0. So definieren wir die zahl3 auch zu Beginn. Sie ist dann aber nicht leer, wie man vermutet. Es werden 8 Bit reserviert, die jeweils auf 0 gesetzt werden.
Definiert man Variablen nicht, wird zwar der Speicherplatz reserviert. Jedoch kann es sein, dass dort bereits Bits gesetzt (also 1) sind. Man nennt das „einen undefinierten Zustand“. Nutzt man diese Variable dann für eine weitere Berechnung, bevor man sie selbst überschreibt, erhält man unter Umständen ein falsches Resultat.
Der Quellcode sieht dann so aus:
byte zahl1 = 200; byte zahl2 = 200; byte zahl3 = 0; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Tipp: Da wir unsere Variablen außerhalb dieser Funktionen gleich zu Beginn deklariert haben, handelt es sich um globale Variablen. Sie sind also global überall verfügbar. Schreibt man eigene Funktionen und deklariert darin Variablen, sind sie nur in dieser Funktion verfügbar und werden lokale Variablen genannt. Verlässt das Programm diese Funktion, wird der dafür verwendete Speicher wieder freigegeben und der Zugriff darauf ist nicht mehr möglich.
Tipp: Kommentare werden durch zwei nach rechts gestellte Schrägstriche erzeugt (auch Slash genannt). Diese nutzt man für Erklärungen des Quellcodes oder Hinweise an andere Programmierer. Auch wenn man selbst Dinge im Code testen möchte, nutzt man das häufig. Das wird dann „auskommentieren“ genannt.
An dieser Stelle soll kurz erklärt werden, was loop() und setup() bedeuten. An den runden und geschweiften Klammern erkennt man, dass es sich um Funktionen (je nach Programmiersprache auch Methoden genannt) handelt. Diese beiden sind in der Arduino IDE zwingend notwendig.
Die setup()-Funktion wird beim Start des Mikrocontrollers einmal durchlaufen. Sie dient der Initialisierung von z.B. Ein- und Ausgängen und wie in unserem Fall der Seriellen Schnittstelle. Die loop()-Funktion bedeutet übersetzt „Schleife“. Hier wird dafür gesorgt, dass der Mikrocontroller nicht angehalten wird. Normalerweise programmiert man das selbstständig. Die Arduino IDE übernimmt das für den Programmierer.
Zu Beginn sehen wir unsere deklarierten und definierten Variablen. Im Setup steht Serial.begin(115200). Serial ist ein Objekt bzw. eine Klasse (hier erkennt man, dass es sich um die Sprache C++ handelt), die es erlaubt, den Arduino über die serielle Schnittstelle (also in dem Fall USB) mit dem verwendeten Computer zu kommunizieren. Mit der Funktion begin() innerhalb der Klasse wird die Schnittstelle initialisiert. Der Zahlenwert in den Klammern stellt die Übertragungsgeschwindigkeit dar.
In der Arduino IDE kann man über den Menüpunkt „Werkzeuge“ oder STRG+UMSCHALT+M den Seriellen Monitor aufrufen. Dort erscheinen dann die Infos aus der Seriellen Schnittstelle. Auf diesem Weg kann man sich Ergebnisse des Arduinos auch auf dem PC-Bildschirm ausgeben lassen. Schreibt man ein größeres Programm mit Zwischenergebnissen, kann man diese Methode auch zum Debuggen nutzen, also zur Fehlersuche (was wir in diesem Fall tun, da unser Ergebnis ja falsch ist).
Die Zeile zur Berechnung sieht so aus, dass rechts vom Zuweisungsoperator „=“ die Addition durchgeführt und dann in die Variable auf der linken Seite geschrieben wird. Also immer von rechts nach links. Wir addieren zahl1 mit zahl2 und schreiben das Ergebnis in zahl3. Es ist wichtig, auf Groß- und Kleinschreibung zu achten. Der Compiler ist da sehr penibel. Umlaute sind ebenfalls nicht erwünscht. Für die bessere Lesbarkeit sollten Zeilen eingerückt werden. Dafür gibt es auch ausführlich beschriebene Konventionen.
In der nächsten Zeile erscheint wieder Serial allerdings mit println(). Diese Funktion steht für print line. Also das Ausgeben einer Zeile mit anschließendem Zeilenumbruch. Es gibt mehrere Möglichkeiten für die Ausgabe auf dem Monitor. Dazu zählen noch Serial.print() (ohne Zeilenumbruch) und Serial.write(), womit Bytes in die Serielle Schnittstelle geschrieben werden.
Laden wir das Programm nun auf den Arduino Nano und öffnen den Seriellen Monitor, erscheint die Zahl 144.
Tipp: Das Öffnen des Seriellen Monitors startet den Arduino neu.
Tipp: Unten rechts im Fenster des Seriellen Monitors muss für die Geschwindigkeit in Baud der gleiche Wert wie im Programmcode Serial.begin(115200) eingestellt sein. Hat man das nicht getan, erscheinen undefinierbare Zeichen im Monitor. Das ist der erste Hinweis darauf, dass die Übertragungsrate nicht übereinstimmt.
Abbildung 15: Arduino IDE - Serieller Monitor
Wir haben nun erfahren, dass ein falsch gewählter Datentyp zu Fehlern führen kann. Es ist also empfehlenswert, vorher zu überlegen, wofür man die Variablen benötigt. In der Arduino IDE ist das Byte als Datentyp definiert. Der Compiler erkennt daEin weiterer Typ mit der gleichen Größe wie ein Byte ist das Char. Das ist die Abkürzung für Character und bedeutet übersetzt Zeichen oder Symbol.
Wie kann man Zahlen als Buchstaben darstellen? Indem man jeder Zahl einen Buchstaben zuordnet. Dafür existiert die sogenannte ASCII-Tabelle. Ausführliche Informationen dazu findet man unter anderem bei Wikipedia. Als Beispiel dient hier der Buchstabe (großes) A. Laut Tabelle entspricht er der Zahl 65. Als Binärcode wäre das 01000001. Wir ergänzen den Quellcode durch die Deklaration und Definition eines Zeichens. Hier kann man sehen, dass als Zuweisung die Zahl 65 gewählt wurde. Das dient in diesem Fall der Verdeutlichung, dass Zeichen einem Zahlenwert zugeordnet sind. In den weiteren Zeilen wurden zusätzliche Beispiele für die Ausgabe auf dem Monitor hinzugefügt.
byte zahl1 = 200; byte zahl2 = 200; byte zahl3 = 0; char buchstabe1 = 65; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Überträgt man nun das kompilierte Programm, erscheint auf dem Monitor A: 144.
Tipp: Die Definition der Variablen buchstabe1 sollte man üblicherweise so formulieren: char buchstabe1 = 'A‘.
Möchte man nun statt einzelnen Zeichen ganze Sätze nutzen, ist das durch einen String möglich. Das bedeutet übersetzt „Zeichenkette“. Hier erkennt man schon am Namen, dass mehrere Zeichen zusammengesetzt werden. Somit ist die Größe eines Strings immer die Anzahl der Zeichen multipliziert mit 8 Bit.
Wir ergänzen den Quellcode durch die Deklaration und Definition des Strings satz1 mit dem Inhalt „Das Ergebnis lautet:“. Außerdem erweitern wir die Ausgabe der Seriellen Schnittstelle durch diese Variable. Die Zeile Serial.print(": "); enthält einen String innerhalb der Klammern. Dazu weiter unten mehr.
byte zahl1 = 200; byte zahl2 = 200; byte zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Auf dem Monitor sollte nach Kompilieren und Hochladen der neu hinzugefügte Satz vor dem Ergebnis erscheinen.
Tipp: Einzelne char werden mit einfachen Anführungszeichen (Apostroph) definiert. Strings mit doppelten Anführungszeichen.
Der Compiler zeigt keine Fehler, unser Ergebnis ist jedoch falsch. Um das zu beheben, können wir die Datentypen ändern. Somit erhalten wir für unsere Werte mehr Speicher. Ein oft verwendeter Typ ist das Integer. Eine Ganzzahl (also glatte Zahlen ohne Komma), die abhängig vom verwendeten System verschiedene Größen einnehmen kann.
Es ist also wichtig, an dieser Stelle schon mal einen Blick in das Datenblatt des Mikrocontrollers bzw. des darauf verwendeten Prozessors geworfen zu haben. Für die meisten Arduinos mit ATmega Prozessor wie dem Uno oder Nano ist ein Integer 16 Bit und damit 2 Byte groß. Wir verdoppeln die Größe des Datentypen, haben aber nun einen Wertebereich von maximal 2^16 und somit 65536. Allerdings auch hier von 0 bis 65535.
Tipp: Auf diese Weise kann man relativ schnell den Wertebereich errechnen: „2 hoch Bitgröße“.
Wir ändern unseren Quellcode so, dass unsere Zahlenvariablen nicht mehr vom Typ byte, sondern Integer sind. Das wird abgekürzt durch „int“. Unser Ergebnis von 400 passt in den Wertebereich eines Integers. Anschließend laden wir das Programm auf den Arduino.
int zahl1 = 200; int zahl2 = 200; int zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Das Ergebnis sollte nun korrekt ausgegeben werden.
Nun wird man eventuell darüber stolpern, dass wir alle drei Variablen in int geändert haben. Die ersten beiden Zahlen passen vom Wert her in ein Byte. Nur unser Ergebnis braucht mehr Speicherplatz. Eine wichtige Regel ist, dass alle Datentypen, die miteinander verrechnet werden, gleich sein sollten. Ist man irgendwann in der Situation, dass man zwei verschiedene Datentypen miteinander verrechnen möchte, muss man einen „Cast“ durchführen.
Das geschieht, indem man vor die Variable in Klammern den neuen Datentypen stellt. Ändern wir unseren Quellcode so, dass wir die beiden Operanden in der Zeile für die Addition von byte nach int „casten“.
byte zahl1 = 200; byte zahl2 = 200; int zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = (int)zahl1 + (int)zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Es stehen in der Arduino IDE auch Funktionen zur Typkonvertierung zur Verfügung. Die Schreibweise wäre dann hier int(zahl1).
Lässt man das Casten weg, wird der Arduino trotzdem noch das richtige Ergebnis anzeigen. Die Speichergrößen passen, der Prozessor hat damit augenscheinlich keine Probleme. Korrekt programmiert ist es jedoch nicht, da in anderen Fällen mit umfangreicheren Berechnungen Fehler auftreten können. Normalerweise wird der Compiler auch eine Warnung ausgeben.
Wir wollen nun unsere Variablen vergrößern von je 200 auf je 30.000 und diese addieren. Wir wissen nun, dass 8 Bit und maximal 255 nicht ausreichen werden. Wir ändern also den Datentyp von byte in int. Wir haben nun Werte bis maximal 65535 zur Verfügung. 30.000 liegt innerhalb dieser Grenze. Wir ändern den Quellcode dementsprechend. Das Casten aus der Addition können wir wieder entfernen.
Tipp: Hier sieht man, warum man Variablen verwendet, statt echte Werte in die Zeile mit der Berechnung einzutragen. Das Ändern von Werten kann an einer übersichtlichen Stelle durchgeführt werden, statt lange im Quellcode nach der richtigen Programmzeile zu suchen. In diesem Fall noch nicht auffällig. Bei umfangreicheren Programmen zwingend notwendig.
int zahl1 = 30000; int zahl2 = 30000; int zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Das angezeigte Ergebnis ist jedoch nicht 60.000, obwohl auch dieser Wert noch unter dem eigentlichen Maximum liegt. Das Problem hierbei ist, dass bei verschiedenen Datentypen wie int unterschieden wird zwischen „signed“ und „unsigned“. Das bedeutet vorzeichenbehaftet und vorzeichenlos. Mit vorzeichenlos versteht man, dass ausschließlich positive Werte angenommen werden. Mit Vorzeichen ist dann das Minus und negative Werte gemeint.
Tipp: Variablen sind standardmäßig „signed“ deklariert.
Tipp: Binäre negative Zahlen erkennt man daran, dass das höchste Bit (im Normalfall das Bit an der links äußersten Stelle) den Zustand 1 hat. Ein Integer z.B. hat trotzdem nur die Größe von 16 Bit. Dort muss man negative und positive Werte unterbringen. Deklariert man eine int-Variable, kann sie standardmäßig nur Werte von -32.768 bis 32.767 einnehmen.
Unser erwartetes Ergebnis von 60.000 passt also nicht in diesen Bereich. Um das richtige Ergebnis zu erhalten müssen wir den Wertebereich verschieben. Dadurch können wir dann Werte von 0 bis 65535. Uns fehlen nun die negativen Werte, können aber unser Ergebnis korrekt abbilden. Um das zu erreichen, ändern wir die Deklarationen der Variablen von int in unsigned int und laden das Programm erneut auf den Nano. Hier ist ebenfalls darauf zu achten, dass alle Variablen vom gleichen Datentyp sind.
unsigned int zahl1 = 30000; unsigned int zahl2 = 30000; unsigned int zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Das Ergebnis sollte nach dieser Aktualisierung korrekt angezeigt werden.
Um negative Werte zu berechnen, müssen die Variablen vorzeichenbehaftet deklariert sein. Wie oben beschrieben, kann man das signed dabei einfach weglassen. Um nun größere Zahlen verwenden zu können, muss man erneut den Datentyp ändern. Dafür steht das „long“ zur Verfügung. Es hat eine Speichergröße von 32 Bit bzw. 4 Byte. Somit sind Werte von -2.147.483.648 bis 2.147.483.647. Für ein vorzeichenloses Long ohne negative Werte reicht der Bereich von 0 bis 4.294.967.295.
Mit der entsprechenden Deklaration sieht der Quellcode dann wie folgt aus. Wir ändern nur die ersten drei Zeilen.
Tipp: Für ein Long muss neben des Datentyps auf der linken Seite auch ein L an das Ende des Zahlenwertes geschrieben werden.
long zahl1 = 30000L; long zahl2 = 30000L; long zahl3 = 0L; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Trotz vorzeichenbehaftetem Datentypen erhalten wir das richtige Ergebnis, nachdem wir die Aktualisierung auf den Arduino hochgeladen haben. Der Wertebereich ist also groß genug.
Bis hier her haben wir nur Ganzzahlen berechnet. Die meisten Sensoren, die man an den Arduino anschließen kann, liefern allerdings gebrochene Zahlen mit Werten hinter dem Komma. Dafür gibt es die Datentypen double und float. Für die Arduinos mit ATmega haben beide eine Speichergröße von 32 Bit und ermöglichen einen Wertebereich von -3,4028235E+38 bis 3,4028235E+38. Vorzeichenlose Kommazahlen gibt es nicht.
Tipp: Je größer der Wert vor dem Komma, desto weniger Nachkommazahlen sind möglich, da der Speicherbereich nicht überschritten werden darf.
double zahl1 = 3.24; double zahl2 = 2.76; double zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Wir ändern unsere Deklarationen, um Kommazahlen zu erhalten:
Nachdem das Programm hochgeladen wurde, zeigt der Monitor den Wert 6.0 an. Obwohl es sich eigentlich um eine glatte Zahl handelt, werden das Komma und eine 0 dahinter ausgegeben. Der Datentyp wird strikt eingehalten.
Tipp: Die Ausgabe der Dezimalzahlen ist in Serial.println() standardmäßig auf zwei Nachkommastellen eingestellt. Man kann einen weiteren Parameter hinzufügen und die Anzahl beliebig verändern. Das sieht dann so aus: Serial.println(zahl3, 5);
Es ist möglich, Kommazahlen in Ganzzahlen zu ändern. Wir wollen dazu das Casten nutzen, um eine weitere Fehlerquelle zu zeigen.
double zahl1 = 3.2; double zahl2 = 2.1; double zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = zahl1 + zahl2; Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Wir ändern die Zahlenwerte der Operanden in den ersten zwei Zeilen:
Das angezeigte Ergebnis ist 5.30. Diesen Wert möchten wir nun runden. Wir ändern den Datentyp für das Ergebnis in Ganzzahlen, also int und casten das Ergebnis der Addition ebenfalls nach int. Dafür setzen wir die Berechnung in klammern und schreiben (int) davor. Somit findet das Casten erst nach der Berechnung statt.
Das angezeigte Ergebnis lautet 5. Wir ändern nun die Werte in den ersten zwei Zeilen erneut.
double zahl1 = 3.4; double zahl2 = 2.3; int zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = (int)(zahl1 + zahl2); Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Normalerweise würde als Ergebnis 5,7 sein. Aufgerundet also 6 als ganze Zahl.
Das angezeigte Ergebnis ist jedoch nicht 6, sondern 5. Das Casten kann also nicht wirklich für das Runden verwendet werden. Die Kommastellen werden lediglich abgeschnitten. Ist man darauf angewiesen, dass korrekt gerundet wird, muss man die Funktion round() verwenden. Wir ändern den Quellcode in der Zeile mit der Addition dementsprechend:
Das Ergebnis sollte nun korrekt mit 6 angezeigt werden.
double zahl1 = 3.4; double zahl2 = 2.3; int zahl3 = 0; char buchstabe1 = 65; String satz1 = "Das Ergebnis lautet:"; void setup() { // put your setup code here, to run once: Serial.begin(115200); zahl3 = round(zahl1 + zahl2); Serial.println(satz1); Serial.print(buchstabe1); Serial.print(": "); Serial.println(zahl3); } void loop() { // put your main code here, to run repeatedly: }
Zu den weiteren Folgen:
Arduino IDE - Programmieren für Einsteiger - [Teil 1]
Arduino IDE - Programmieren für Einsteiger - [Teil 3]
12 Kommentare
Markus
Klasse Artikel. Vielen Dank. Bitte weitermachen mit Teil 3!
Helmut
An den Moderator :
In meinem Kommentar ist auf dem Übertragungsweg oder bei der Moderation ein Teil verloren gegangen und/oder verändert worden.
So wie es jetzt dasteht:
“Multiplikation mit 2 <> 1x Linksschieben Diviision durch 2” ist das verwirrend !
Es muß heißen:
Multiplikation mit 2 = 1x Linksschieben
Diviision durch 2 = 1x Rechtsschieben
Bitte korrigieren. Danke.
Helmut Schenk
Rolf Wiegmann
Momentan ist es für mich noch kein Neuland, aber wenn es so gut weiter geht werde ich noch sehr viel lernen. Und das mit Spaß.
Weiter so
Helmut
Zitat aus dem Kommentar von hafl:
“besser: Tipp: shift ist eine schnelle Variante der Multiplikation mit 2 (right) bzw. Division durch 2 (left)”
Das ist falsch.
Richtig:
Multiplikation mit 2 <> 1x Linksschieben Diviision durch 2
Andreas Wolter
@Jürgen: du liegst natürlich richtig. Im Grunde ist es egal, in welcher Richtung man es liest. Man muss nur die Wertigkeiten zusammenrechnen. Rechnet man große Zahlen um, ist es wahrscheinlich einfacher, mit den hohen Wertigkeiten zu beginnen. Das kann im Grunde jeder so machen, wie er es für sich besser umsetzen kann. Da die Wertigkeiten von rechts nach links aufsteigend sind, wurde hier diese Richtung gewählt, damit es eventuell besser nachzuvollziehen ist.
Der kleine Dreher, der sich dort eingeschlichen hatte, wurde bereits behoben. Danke für den Hinweis!
Manfred
Super Beitrag. Ich bin Anfänger und lerne noch.Habe viel aus diesen 2. Teil gelernt. Hoffe der 3. Teil wird genau so gut. Bitte so weiter machen. VG. Manfred
Steigleder
Es muß 1+0+4+8 = 13 heißen und nicht 1+4+0+8 = 13. Manch Neuling könnte sonst schon jetzt das Handtuch werfen.
Wrzlbrnft
Ein sehr gut gemachter Beitrag. Ich freue mich auf den nächsten.
hafl
So muss eine Einführung geschrieben sein. Gut nachvollziehbarer Text mit klarer Struktur.
Kleine Ergänzung
Text: Tipp: shift ist eine schnelle Variante der Multiplikation mit 2
besser: Tipp: shift ist eine schnelle Variante der Multiplikation mit 2 (right) bzw. Division durch 2 (left)
Purps
Sehr guter Beitrag, daran solltet ihr unbedingt festhalten… Darauf haben bestimmt viele gewartet… Weiter so….
Hans Zethofer
Binär 0 0 0 0 0 1 1 0 1 Von rechts nach links:
1 + 4 + 0 + 8 = 13
sollte 1+0+4+8 lauten !!
Jürgen
Bei der Erklärung des Binären Zahlensystemes kam der Schreiberling wohl etwas durcheinander mit der Reihenfolge beim lesen von Binärcode (ich habe binären Code noch nie von rechts nach links gelesen) und durcheinander beim ‘addieren’ der Bits zum ermitteln des dezimalen Zahlenwertes (im Ergebnis richtig, im Rechenweg falsch).