Sturzi's Arduino-Grundlagen-Thread #0 für Programmierer-Neueinsteiger

  • Ich habe jetzt schon von einigen, die das Projekt #1 mitverfolgen, gehört, dass sie mit abtippen und ausprobieren durchaus erfolgreich sind, aber dass das Verständnis für das Programm (noch) fehlt.


    Diesem Umstand will ich mit diesem Thread Rechnung tragen. Ich werde (ohne grossen Schaltungsaufbau), sondern vor Allem mit Hilfe des Serial Monitors Schritt für Schritt (begonnen mit den Basics) die Programmierung erforschen.


    Die Bereitstellung der Entwicklungsumgebung (IDE) setze ich voraus. Nötigenfalls kann man das in meinem Projekt-Thread #1 oder in zahlreichen Tutorials nachlesen.


    Wir werden uns zu Beginn vor Allem mit Variablen und dem Programm-Ablauf befassen.


    Dieser Thread ist nicht für Leute mit Programmier-Erfahrung, sondern für solche, die bisher keine Programmierer-Kenntnisse hatten. Er soll die Verständnis-Schwierigkeiten in den anderen Projekt-Threads mindern.

  • Programm-Ablauf: setup() und loop()


    Jedes Arduino-Programm hat zwei vordefinierte Funktionen, nämlich setup() und loop(). Diese Funktionen sind vorgesehen, dass wir, die Arduino-Programmierer sie implementieren. Implementieren heisst hier, dass wir den benötigten Programm-Code einfügen, damit der für uns Arduino das macht, was wir gern möchten.


    Wie erkennen wir Funktionen? Funktionen erkennt man an folgenden Elementen:

    - Typ (hier void)

    - Name (hier setup und loop)

    - Rundes Klammern-Paar nach dem Namen. In diesem Klammern-Paar darf etwas stehen oder (wie hier) auch nichts.

    - Geschweiftes Klammern-Paar nach dem runden Klammern-Paar. In diesem Klammern-Paar darf etwas stehen oder (wie hier) auch nichts.


    Code
    void setup() {
    
    }
    
    void loop() {
    
    }

    Nach dem Hochladen des Programmes auf den Arduino, führt dieser zuerst alles aus, was wir im geschweiften Klammern-Paar der Funktion setup() geschrieben haben.


    Anschliessend führt er das aus, was wir im geschweiften Klammern-Paar der Funktion loop() geschrieben haben. Wenn er damit fertig ist, macht er das nochmals und nochmals ...


    Was passiert, wenn wir in beiden Funktionen nichts geschrieben haben, wie im obigen Beispiel , und das Programm trotzdem ausführen?

    Genau nach oben beschriebenem Muster: Er geht ins setup() und macht dort nichts. Dann geht er ins loop() und macht dort in rasender Geschwindigkeit immer und immer wieder nichts.


    Das Nichtstun im loop() dauert an, bis man den Arduino vom Strom nimmt, die Reset-Taste drückt oder ein neues Programm auf den Arduino hochlädt.


    Nebst den beiden vordefinierten Funktionen setup() und loop() steht es uns frei, weitere (neue) Funktionen zu definieren. Auch diese müssen sich ans Muster mit den vier Elementen halten.

  • Variablen - Folge 1


    Variablen haben einen Typ und einen Namen.


    Beispiel:

    int hoppla;


    Hier haben wir eine Variable vom Typ int mit dem Namen hoppla deklariert.

    Diese Variable nimmt im Arbeitsspeicher des Arduino zwei Bytes in Anspruch, weil sie vom Typ int ist. Ein int beim Arduino braucht immer zwei Bytes.


    Man kann die Variable bei der Deklaration auch gleich auf einen bestimmten Wert setzen:

    Code
    int hoppla = 4711;
    
    void setup() {
      Serial.begin(9600);
      Serial.println(hoppla);
    }
    
    void loop() {
    
    }

    Wenn man dieses Programm ausführt sieht man, dass der Wert von hoppla tatsächlich 4711 ist.


    Der Wertebereich von einem int ist -32768 ... 32767.


    Wenn man hoppla nun auf 47110 setzt und das Programm wieder ausführt erscheint nicht 47110, sondern etwas ganz Abartiges. Das ist, weil wir uns nicht an den Wertebereich eines int gehalten haben.


    Beim C++ ist der Programmierer verantwortlich dafür, dass das nicht passieren kann.


    Man kann die Variable aber auch nachträglich zuordnen:

    Wenn man dieses Programm ausführt, erscheint der richtige Wert -9876 im Serial Monitor.

  • Variablen - Folge 2


    Variablen sind variabel. Der Name sagt es schon. Sie können im Verlauf der Programm-Ausführung ihren Wert auch verändern.


    Hinweis: Variablen-Name dürfen keine Umlaute enthalten. Darum habe ich zaehler geschrieben.


    Hier sieht man schön, wie das Hochzählen vorerst funktioniert und wenn man dann den zulässigen Wertebereich des int überschreitet, dann passiert es.


    Wenn man nun aus dem int einen long macht, dann funktioniert es auch für grössere ganze Zahlen:

    Variablen vom Typ long belegen im Speicher 4 Bytes. Ihr Wertebereich ist -2147483648 ... 2147483647.

  • Variablen - Folge 3 - Variablen-Namen


    Variablen-Namen dürfen aus Buchstaben, Ziffern und dem Underscore-Zeichen '_' bestehen. Sie dürfen jedoch nicht mit einer Ziffer beginnen. Buchstaben, die in der englischen Sprache nicht vorkommen, z.B. Umlaute, dürfen auch nicht in Variablen-Namen eingesetzt werden. Die Länge der Namen ist zwar eingeschränkt, das Limit ist aber so gross, dass man in der Praxis das Limit nie erreicht.


    Namen, die bereits vergeben sind, darf man natürlich auch nicht für eigene Variablen verwenden. Alle Namen, die man in der Arduino-Sprechreferenz findet, sind bereits vergeben. Dazu gibt es einige C++ Schlüsselwörter, die dort nicht aufgeführt sind. Es ist nicht tragisch, wenn man nicht alle Namen kennt, die man nicht für eigene Variablen verwenden darf. Der Compiler sagt es schon, wenn es ihm nicht passt.


    Alle Namen sind case-sensitiv. D.h. die Gross- / Kleinschreibung ist strikt. Also für den Compiler sind pinUsedForLED und PinUsedForLED zwei verschiedene Namen.


    In der professionellen C++ Programmierung ist es weltweit üblich, dass Variablen-Namen mit einem Kleinbuchstaben beginnen und dann die Folge-Wörter in natürlicher Gross- / Kleinschreibung fortgesetzt werden. Viele Programmierer setzen die Variablen-Namen aus englischsprachigen Wörtern zusammen. Diese Konventionen sind nicht zwingend. Der Compiler prüft sie nicht.


    Globale Variablen sind solche, die im ganzen Programm sichtbar sind. Dazu müssen sie ausserhalb der Funktionen, z.B. ganz am Beginn des Programmes deklariert werden. Globale Variablen sollte einen sprechenden, möglichst selbsterklärenden Namen haben, z.B. millisToGo oder millisOnLastChange. Für globale Variablen sind z.B. i, j, k, abc, ... schlechte Namen.


    Lokale Variablen sind solche, die innerhalb einer Funktion definiert werden. Diese sind nur in dieser Funktion sichtbar. Auch für lokale Variablen sind in der Regel sprechende Namen sinnvoll. Es ist allerdings üblich, Loop-Lauf-Variablen (was das ist, kommt später) mit z.B. i, j oder k zu benennen.

  • Datentypen - Folge 1


    Es gibt im C++ viele Datentypen. Für die Arduino-Programmierung sind längst nicht alle möglich, nützlich und sinnvoll.


    Hier die numerischen Datentypen, die für die Arduino-Programmierung möglich und sinnvoll sind. Es bestehen leichte Unterschiede bei den verschiedenen Generationen des Arduino. Die folgenden Angaben sind gültig für den Arduino Uno, den Nano und den Mega.


    byte für ganze Zahlen im Bereich 0 ... 255, benötigt 1 Byte im Speicher

    int für ganze Zahlen im Bereich -32768 ... 32767, benötigt 2 Bytes im Speicher

    unsigned int für ganze Zahlen im Bereich 0 ... 65535, benötigt 2 Bytes im Speicher

    long für ganze Zahlen im Bereich -2147483648 ... 2147483647, benötigt 4 Bytes im Speicher

    unsigned long für ganze Zahlen im Bereich 0 ... 4294967295, benötigt 4 Bytes im Speicher


    float für gebrochene Zahlen mit einer Genauigkeit von etwa 6 geltenden Ziffern (*), benötigt 4 Bytes im Speicher

    double ist bei den Arduinos Uno, Nano und Mega dasselbe wie float. Macht hier also keinen Sinn.


    (*) Gebrochene Zahlen und geltende Ziffern:

    Gebrochene Zahlen sind solche, die einen Dezimalpunkt enthalten (in der Programmierung ist es immer ein Punkt, nie ein Komma).

    Für die Ermittlung der Anzahl geltender Ziffern wie folgt vorgehen:

    Bei der Zahl erst den Punkt wegdenken, dann alle führenden (von links her vorwärts) und hängenden (von rechts her rückwärts) Nullen wegdenken. Was übrig bleibt, sind die geltenden Ziffern.


    Beispiele:

    Die Zahl 3.14159 hat 6 geltende Ziffern

    Die Zahl 0.0328 hat 3 geltende Ziffern

    Die Zahl 9943000.0 hat 4 geltende Ziffern

    Die Zahl 0.0000000000012 hat 2 geltende Ziffern

    Die Zahl 3400000000000.0 hat 2 geltende Ziffern

    Die Zahl 20.5672615 hat 9 geltende Ziffern. Wenn man diese in einem float speichert, gehen die hintersten Ziffern verloren.


    Bei der Speicherung von gebrochenen Zahlen mit float gibt es auch einen Werte-Bereich. Dieser ist aber so gross, dass wir uns für normale Arduino-Projekte darüber keine Gedanken machen müssen.


    Bei byte, int und long ist es der Wertebereich, der uns einschränkt. Bei float ist es die Genauigkeit, die uns einschränkt.

  • Datentypen - Folge 2


    In diesem Beitrag geht es um den Datentyp bool. Der Begriff bool kommt aus der Boolschen Algebra und hat zu tun mit Aussagenlogik. Eine Aussage im Sinne der Aussagenlogik kann wahr oder falsch sein (true oder false). Deshalb besteht der Wertebereich beim Typ bool nur aus den beiden Werten true oder false.


    In der Arduino-Umgebung gibt es auch den Datentyp boolean. Das ist dasselbe wie bool. Empfehlung: bool verwenden.


    Variablen vom Typ bool belegen im Speicher 1 Byte.


    Beispiele:

    Code
      bool lightIsOn;
      bool motorIsRunning;
      bool directionIsForward;

    An den gewählten Namen dieser Variablen erkennt man etwa, wie es gemeint ist. Wenn der Motor rückwärts läuft und das Licht aus ist, kann man das im Programm festhalten mit den Anweisungen:


    Code
      motorIsRunning = true;
      directionIsForward = false;
      lightIsOn = false;


    Was man dann damit machen kann, dürfte an dieser Stelle schwierig sein, zu erkennen.


    Don't worry! Wir müssen uns erst mit Expressions (Ausdrücken) und Conditions (Bedingungen) befassen, dann wird es schnell klar. Schwierig ist es nämlich überhaupt nicht.

  • Variablen - Folge 4 - Konstanten


    Hey, was soll das? Sprechen wir jetzt über Variablen oder über Konstanten? Das sind doch Gegensätze!

    Jein, denn im C++ sind Konstanten nichts anderes als Variablen, die nicht variabel sind :) , also Variablen, die man zur Laufzeit des Programmes nicht mehr verändern kann.


    Will man eine Konstante definieren, macht man das wie bei einer Variablen, aber man setzt das Keyword const davor.

    Weil man die Konstante ja nicht mehr verändern kann, muss man sie zusammen mit der Deklaration auch gleich initialisieren. Ansonsten könnten wir sie nie mehr auf einen definierten Wert setzen.


    Code
      const byte PIN_NUMBER;        // Blosse Deklaration der Konstanten
      PIN_NUMBER = 17;

    So geht es nicht. PIN_NUMBER ist eine Konstante und kann nach der Deklaration nicht mehr auf einen bestimmten Wert gesetzt werden. Auf Zeile 2 motzt der Compiler.


    Code
      const byte PIN_NUMBER = 17;    // Deklaration mit Definition der Konstanten

    So funktioniert es. Der Compiler nimmt die Definition der Konstanten zur Kenntnis und initialisiert sie auch gleich auf den Wert 17.


    Wofür brauchen wir Konstanten? Nehmen wir als gutes Beispiel mal die im Arduino fest integrierte LED, die mit dem Pin 13 fest verdrahtet ist. Arduino hat für uns bereits eine Konstante LED_BUILTIN definiert und sie auf den Wert 13 gesetzt. Jetzt können wir sie in unserem Programm über den Namen LED_BUILTIN ansprechen, statt über die Nummer 13. Das ist erstens mal sprechender, d.h. der Name sagt schon, was es ist und zweitens wenn man sich im Programm vertippt, merkt es der Compiler und motzt. Würden wir die Zahl 13 brauchen und vertippen uns auf 14, hat der Compiler keine Chance, das zu merken (*). Es gibt noch weitere gute Gründe, Konstanten zu verwenden. Aber für den Moment nehmt das einfach zur Kenntnis: Professionelle Programmierer verwenden symbolische Konstanten (wie hier LED_BUILTIN) wenn immer es geht und vermeiden Literals (absolute Werte wie hier 13) im ausführbaren Programm-Code nach Möglichkeit. Beachte dazu auch diesen Beitrag im Projekt #1.


    Im Gegensatz zu Variablen- und Funktions-Namen werden Konstanten in der Regel all-capital (durchgehend gross) geschrieben. Zur Trennung der einzelnen Wort-Teile im Namen verwendet man das Underscore-Zeichen '_'. Das ist zwar keine Vorschrift und wird vom Compiler nicht durchgesetzt. Aber es ist eine weitverbreitete Konvention und viele Firmen verlangen das von ihren Programmierern in ihren Programming-Guides.


    (*) Wenn man Fehler macht, ist es oft mühsam, diese zu finden, damit man sie korrigieren kann. Wenn man möglichst so programmiert, dass der Compiler die Fehler finden kann, ist das die beste Wahl. Die vom Compiler beanstandeten Fehler behebt man in der Regel leicht. Mühsamer wird es, wenn der Compiler die Fehler nicht finden kann und sie erst bei der Ausführung des Programmes zuschlagen, also wenn das Programm macht, was man programmiert hat und nicht das, was man eigentlich wollte :) .

  • Danke Röbi, ich glaube zu wissen, was Dich zu diesem Beitrag inspitiert hat. :whistling:


    Dieser Thread hier ist für mich sehr hilfreich, in den anderen kann ich das Tempo nicht ganz halten. Das macht aber gar nichts, die Beiträge bleiben ja stehen und jeder interessierte kann sich in seinem Tempo durcharbeiten.

    Gruess Martin

  • Auch ein grosses Dankeschön von mir an Dich Röbi.

    Mir geht's aktuell wie Martin, dass ich im Beitrag 1 nicht mehr weitergekommen bin.

    Für Anfänger wie mich, sind Deine Erklärungen sehr verständlich und gut nachvollziehbar.

    Gruss Urs

    __________________________________________________________________________________

    34 Tank-SCC / 4 Kasten-SCC und noch viele Nummernvarianten möglich :D:whistling:

    Wer mehr über diese Materie wissen möchte: www.containercars.ch

  • Assignment (Zuordnung)


    Das Assignment wird mit einem Assignment-Operator gemacht. Der meist verwendete Assignment Operator ist = (Gleichheitszeichen). In unseren Beispielen haben wir diesen schon öfters gebraucht. Hier wollen wir ihn etwas genauer untersuchen.


    Die Expression (Ausdruck) rechts des Operators wird evaluiert und der resultierende Wert wird der Variablen links des Assignment Operators zugeordnet. Oder etwas einfacher gesagt: Die Variable links wird auf den Wert der Expression rechts gesetzt.


    Beispiele:

    Code
      int var0 = 0;
      int var1 = 15;
      int var2 = 3;
      int var3 = var1 + 1;
      int sum = var1 + var2;
      int prod = var1 * var2;
      float var4 = 3.14;
      float var5 = 22.0 / 7.0;
      float var6 = var4 / 2;

    Zeile 1: var0 wird auf den Wert 0 gesetzt

    Zeile 2: var1 wird auf den Wert 15 gesetzt

    Zeile 3: var2 wird auf den Wert 3 gesetzt

    Zeile 4: Zum Wert von var1 wird 1 dazugezählt und das Ergebnis wird in var3 geschrieben

    Zeile 5: Die Werte von var1 und var2 werden addiert und das Ergebnis wird in sum geschrieben

    Zeile 6: Die Werte von var1 und var2 werden multipliziert und das Ergebnis wird in prod geschrieben

    Zeile 7: var4 wird auf den Wert 3.14 gesetzt

    Zeile 8: Die Division 22.0 / 7.0 wird ausgeführt und das Ergebnis wird in var5 geschrieben

    Zeile 9: Der Wert von var4 wird durch 2 dividiert und das Ergebnis wird in var6 geschrieben


    Bei all diesen Beispielen steht rechts des Assignment Operators eine Expression. Expressions können einfach oder auch sehr umfangreich sein. Wie die Variablen haben auch die Expressions einen Typ. Die Expressions der ersten sechs Beispiele sind vom Typ int und die letzten drei vom Typ float.


    Der Begriff Expression ist sehr wichtig und wir werden den in den nächsten Beiträgen genauer anschauen. Insbesondere werden wir uns mit dem Typ der Expression auseinandersetzten.


    Nun, zurück zum Assignment: Wir haben bis jetzt nur den ganz einfachen Assignment Operator = angeschaut. Daneben gibt es noch weitere. Ich werde dabei nur auf die meist verbreiteten eingehen und diese auch an Beispielen erklären:


    Code
      var0 += 1;
      var1 += var0;
      var2 -= var1;
      var3 *= var2;
      var3 /= 2;

    Zeile 1: macht dasselbe wie var0 = var0 + 1;

    Zeile 2: macht dasselbe wie var1 = var1 + var0;

    Zeile 3: macht dasselbe wie var2 = var2 - var1;

    Zeile 4: macht dasselbe wie var3 = var3 * var2;

    Zeile 5: macht dasselbe wie var3 = var3 / 2;


    Wie man leicht erkennen kann, sind diese zusätzlichen Assignment Operators nicht unbedingt nötig, weil man die entsprechenden Assignments auch anders schreiben kann. Es sind nur Erleichterungen, damit man weniger schreiben muss.


    Fazit: Links des Assignment Operators steht immer eine Variable. Der Inhalt (Wert) der Variablen wird dabei verändert (abhängig von der Expression und vom Assignment Operator).

  • Expressions - Folge 1


    Expressions sind beim Erlernen des Programmierens ein äusserst wichtiges Konzept. Ohne zu wissen, was Expressions sind und ohne sie zu erkennen, kommt man im Programmieren nicht sehr weit. Wir brauchen diesen Begriff zwingend, um hier mit den Grundlagen weiterzukommen.


    Eine Expression (Ausdruck) ist ein Teil eines Statements (Anweisung), der in einem Wert resultiert. Nun, was heisst das?

    Ich erkläre es am Besten mit ein paar Beispielen:


    Code
    int wert1 = 15;
    int wert2 = 17 - 11;
    int wert3 = (wert1 + wert2) / 2;
    float pi = 3.1416;
    float durchmesser = 9.7;
    float radius = durchmesser / 2;
    float umfang = durchmesser * pi;
    float flaeche = radius * radius * pi;

    In diesen Beispielen ist die Expression immer der Teil zwischen dem Assignment Operator und dem abschliessenden Semikolon. Also

    Zeile 1: 15

    Zeile 2: 17 - 11

    Zeile 3: (wert1 + wert2) / 2

    Zeile 4: 3.1416

    Zeile 5: 9.7

    Zeile 6: durchmesser / 2

    Zeile 7: durchmesser * pi

    Zeile 8: radius * radius * pi


    Jede dieser Expressions resultiert in einem Wert, der in die jeweilige Variable links vom Assignment Operator geschrieben wird.

    Auf den Zeile 1 bis 3 ist der Typ der jeweiligen Expressions int und auf den Zeilen 4 bis 8 ist er float. Das ist aber nicht der Fall, weil links jeweils int oder float steht. Die links geschriebenen Typen gehören zu den Variablen und nicht zu den Expressions. Der Typ der Expressions hängt von deren Inhalt ab und ist unabhängig vom Typ der linken Variablen.

    Wenn die Typen der Variablen und die der Expressions übereinstimmen, funktioniert das Assignment unproblematisch.


    Es kann nun vorkommen, dass eine Variable von einem anderen Typ ist als die Expression, deren Wert man in die Variable schreiben will. Darauf will ich im nächsten Beitrag eingehen.

  • Expressions - Folge 2


    Was passiert, wenn der Typ einer Expression nicht mit dem Typ der Variablen übereinstimmt?


    Code
    float var1 = 22;    // float <-- int
    float var2 = 5.4;   // float <-- float
    int var3 = 22;      // int <-- int
    int var4 = 22.5;    // int <-- float

    Zeile 1: var1 enthält nun 22.0. Eine Ganzzahl geht auch in einen float.

    Zeile 2: var2 enthält nun 5.4. Die Typen stimmen überein.

    Zeile 3: var3 enthält nun 22. Die Typen stimmen überein.

    Zeile 4: var4 enthält nun 22. Der Anteil nach dem Dezimalpunkt geht verloren.

  • Expressions - Folge 3


    Hier geht es noch um logische Expressions vom Typ bool. Diese werden vor Allem bei Programm-Verzweigungen (if) oder bei der Programmierung von Schleifen gebraucht.


    Logische Expressions enthalten Vergleichs- und / oder logische Operatoren.


    Code
    counter >= 0
    var1 == var2
    millisToWait < currentMillis
    flag1 && flag2
    flag3 || flag4

    Hier haben wir 5 logische Expressions. Jede resultiert in entweder true oder false (wahr oder falsch).


    Zeile 1: Resultiert auf true, wenn der Inhalt von counter grösser oder gleich 0 ist

    Zeile 2: Resultiert auf true, wenn die beiden Variablen var1 und var2 den gleichen Inhalt haben

    Zeile 3: Resultiert auf true, wenn der Inhalt von millisToWait kleiner ist als der Inhalt von currentMillis

    Zeile 4: Resultiert auf true, wenn beide Variablen flag1 und flag2 true sind

    Zeile 5: Resultiert auf true, wenn mindestens eine der Variablen flag3 oder flag4 true sind