Arduino IDE - Programmieren für Einsteiger - [Teil 5]

Teil 5

Bisher haben wir gesehen, wie wir eine LED und ein Potentiometer an den Arduino anschließen und die LED mit Pulsweitenmodulation dimmen können. Wir werden nun noch einen Taster hinzufügen und ich zeige Ihnen kurz, welche mechanischen Eigenschaften zu Fehlverhalten führen.

Außerdem erkläre ich Ihnen, warum ein Taster am Arduino nicht genauso funktioniert wie in einem normalen Stromkreis. Wir werden dann eine Statusmaschine implementieren, mit der wir dann die verschiedenen Modi für die der LED durchschalten können. Sie soll auszuschalten sein, blinken oder pulsieren. Als letztes Ziel soll die Helligkeit der LED während des Blinkens direkt verändert und die Pulsgeschwindigkeit manuell eingestellt werden können.

Benötigte Hardware

Anzahl Bauteil Anmerkung
Computer (Windows, Linux, MacOS)
1 Arduino Nano Mikrocontroller
1 Mini-USB Kabel
1 Steckbrett
LED
1 Taster
1 Widerstand 220 Ohm
1 Potentiometer 10 KOhm
1 Verbindungskabel

LED mit Taster ein- und ausschalten

Im nächsten Schritt wollen wir einen digitalen Eingang nutzen, um einen Taster anzuschließen. Er soll vorerst nur die LED ein- und ausschalten. Zuerst ist dabei wichtig, dass die digitalen Eingänge einen Vorwiderstand benötigen. Entweder Pull-up oder Pull-down. Ein einfacher Stromkreis mit Spannungsquelle, Taster und LED würde ungefähr wie im folgenden Bild aussehen:

Abbildung 20: Einfacher Schaltkreis mit Taster und LED

Der Taster schließt den Stromkreis, die LED wird mit Strom versorgt und leuchtet. Schließt man aber nun einen Taster an einen digitalen Pin des Arduinos an, sieht das etwas anders aus.

Wir ergänzen unseren Schaltplan aus Teil 4 um einen Taster. Zu sehen im folgenden Bild:

Abbildung 21: Schaltplan Arduino Nano mit LED, Poti und Taster

 

Betätigt man dann den Taster, wird am Eingangspin ein Kurzschluss erzeugt. Wird der Taster nicht betätigt, ist der Zustand nicht definiert, da ein Pegel anliegt, der nicht genau 0V oder 5V beträgt. Somit ergibt sich ein ungewolltes Verhalten. Wir testen das und erzeugen uns einen Quellcode, der mit dem Taster eine LED ein- und ausschalten soll. Das sieht dann folgendermaßen aus:

const int led_pin =  3;
int taster_pin = 4;
bool input = LOW;

void setup() {
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT);
}

void loop() {
    input = digitalRead(taster_pin); 
    digitalWrite(led_pin, input);
}

Der Taster ist an Pin 4 angeschlossen, der im setup() als Eingang initialisiert wird. mit digitalRead() erhalten wir den Zustand des Eingangspins. Mit digitalWrite() geben wir den Zustand auf den Ausgang mit der LED. Lädt man das Programm auf den Arduino, wird wahrscheinlich die LED irgendwann flackern und dann eingeschaltet bleiben. Der Pegel am digitalen Eingang sollte also klar definiert werden. Wir ändern im Quellcode den Modus für den Eingangspin.

Im setup() wird dafür die folgende Zeile geändert:

pinMode(taster_pin, INPUT);  

in

pinMode(taster_pin, INPUT_PULLUP);

Pull up bedeutet hochziehen. Man zieht das Potenzial auf den Maximalpegel hoch, also 5V. Das entspricht einem logischen HIGH. Das bedeutet aber auch, dass der Wert an dem Eingang im unberührten (nicht aktiven) Zustand immer HIGH ist. Also, wenn man keinen Taster drückt. Das nennt man dann „low active“ (aktiviert, wenn der Pin den Zustand LOW einnimmt). Somit wird die LED immer leuchten, da wir diesen Wert mit der Funktion digitalWrite() direkt auf den Ausgangspin der LED geben. Dem können wir im Quellcode leicht entgegenwirken, indem wir den logischen Zustand des Inputs negieren. Der Quelltext sieht so aus. 

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;

void setup() {
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin); 
    digitalWrite(led_pin, !input);
}

Für den Eingang wurde der interne Pull-up-Widerstand aktiviert und an der Stelle, an der wir mit digitalWrite() die LED schalten, haben wir den Eingang negiert. Das Ausrufezeichen ist hier wieder ein NICHT. Wird der Taster nicht betätigt, steht in der Variablen input der Wert HIGH. Mit dem Ausrufezeichen übergeben wir dem Ausgang ein NICHT HIGH, also LOW. Somit ist die LED im Normalzustand aus. Betätigen wir den Taster, erhalten wir mit digitalRead(taster_pin) den Wert LOW. Mit der Negierung geben wir auf den LED-Pin ein NICHT LOW, also HIGH. Die LED leuchtet.

Tipp: Es ist natürlich auch möglich, Pull-down- oder Pull-up-Widerstände in die elektronische Schaltung einzubauen. Es empfiehlt sich, große Widerstände zu verwenden. Den genauen Aufbau findet man in vielen Tutorials im Internet. Man muss dann mehr Bauteile verwenden und spart sich dadurch die Negierung und den internen Pull-up-Widerstand.

Wir werden den Taster jetzt nutzen, um die LED dauerhaft einzuschalten. Quasi wie mit einem Relais. Wir ergänzen den Quellcode durch die Variable led_status. Der Quellcode sieht dann folgendermaßen aus: 

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;          
bool led_status = LOW;

void setup() {
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin); 
    if (input == LOW) {
        led_status = !led_status;
        digitalWrite(led_pin, led_status);
    }
}

In der Hauptschleife lesen wir in jedem Durchlauf den Wert des Eingangspins. Betätigen wir den Taster, wird dieser Wert LOW sein (wie gesagt, wir haben durch den Pull-up-Widerstand „low active“). Es trifft dann die Bedingung für die folgende if-Anweisung zu. Folglich wird dann der Status der LED umgeschaltet, indem wir durch eine Negierung den Wert von LOW auf HIGH oder umgekehrt ändern. (Die LED ist „high active“, sie leuchtet also, wenn der Wert HIGH ist.) Wir geben als letzten Schritt den Status der LED auf den LED-Pin.

Laden wir das Programm auf den Arduino und betätigen den Taster, werden wir merken, dass die LED manchmal eingeschaltet wird, manchmal nicht oder manchmal flackert sie. Das liegt am physikalischen Verhalten des mechanischen Tasters. Man sagt dazu: „er prellt“. Das heißt, auch wenn man den Taster noch so schnell und fest drückt, er wird im mikroskopischen Bereich immer erst abwechselnd schließen und öffnen, bis er gedrückt bleibt.

Man kann sich das wie ein Sprungbrett vorstellen, das eine Weile weiter wackelt, nachdem der Springer schon lange ins Wasser gesprungen ist. Man kann das in Hardware mit Kondensatoren abfangen oder in Software lösen. Das nennt man dann „entprellen“ oder auf Englisch „debounce“. Wir werden das in Software umsetzen.

Ein weiteres Problem wird uns später treffen, wenn wir den Taster nutzen, um zwischen verschiedenen Modi umzuschalten. Was passiert nämlich, wenn man den Taster gedrückt hält? Es wird dauerhaft durch die Modi geschaltet, bis der Taster losgelassen wird. Zusammen mit dem Entprellen werden wir dieses Problem gleich mit lösen. Dann wird beim Betätigen des Tasters der Modus umgeschaltet, aber man muss den Taster erst loslassen und dann neu betätigen, um erneut den Modus zu wechseln.

Taster entprellen

Wenn man in der Software einen Taster entprellt, muss man im Grunde nur eine gewisse Zeit warten, bis dieser in der Ruhe, also dauerhaft gedrückt ist. Für diese Zeitspanne wird der Eingang des Tasters ignoriert bzw. nicht ausgewertet. Es handelt sich dabei um wenige Millisekunden. Wir können aber auch hier nicht die delay()-Funktion verwenden, da wir den Programmablauf sonst blockieren.

Unsere nächste Überlegung ist nun, was in unserem Programm passieren soll. Es läuft eine Dauerschleife und in jedem Umlauf soll überprüft werden, ob eine gewisse Zeit abgelaufen ist. In dieser Zeit kommt der Taster in die Ruhe. Das Prinzip kennen wir bereits aus dem Beispiel „BlinkWithoutDelay“. Wir legen eine Zeitspanne von 80 ms fest und schreiben sie in die Variable prell_delay. Der Datentyp, den wir für Zeiten brauchen, ist unsigned long. Die Funktion, die wir wieder als endlos laufende Stoppuhr nutzen, ist millis(). Diese arbeitet mit dem genannten Datentypen.

Um das Gedrückthalten abzufangen, müssen wir den neuen Zustand des Tasters mit einem vergangenen Zustand vergleichen. Wir brauchen also neben der Variable input noch eine zweite Variable input_alt. Damit können wir testen, ob sich der Zustand des Tasters verändert hat. Wir ändern nun den Quellcode so, dass wir die if-Anweisung für das Umschalten der LED um weitere Bedingungen ergänzen.

Im Moment testen wir nur, ob der Taster den Zustand LOW hat. Dann schalten wir die LED. Jetzt werden wir vergleichen, ob der Taster zu einem früheren Zeitpunkt den Zustand HIGH angenommen hatte. Beide Bedingungen sollen erfüllt sein. Das erreichen wir mit zwei kaufmännischen &. Erst wenn der neue input LOW ist UND der vorherige input HIGH war, schalten wir die LED um. Damit fangen wir das Gedrückthalten ab. Denn wir müssen erst den Taster loslassen (er geht dann auf HIGH), damit das Innere der if-Anweisung ausgeführt wird.

Jetzt müssen wir noch die Prell-Zeit überbrücken. Allgemein ausgedrückt ist das ähnlich wie mit dem Lampenbeauftragten. Jetzt haben wir einen Tasterbeauftragten. Er notiert sich wieder die Zeit auf der endlos laufenden Stoppuhr. Er kommt alle paar Millisekunden an unserem Raum mit der Lampe vorbei. Dabei schaut er auf die Stoppuhr und subtrahiert von dieser aktuellen Zeit seine zu Beginn notierte alte Zeit. Ist das Ergebnis größer, als die gewünschte Prell-Zeitspanne, darf er den Raum betreten und die Lampe umschalten.

Das ist unsere dritte Bedingung für die if-Anweisung. Die Zeitspanne muss größer sein, als die vorgegebene Prell-Zeit. Auch hier nutzen wir wieder das doppelte kaufmännische & und fügen damit drei Bedingungen zusammen, die erfüllt sein müssen, um die LED umzuschalten.

Wir schreiben den Quellcode wie folgt:

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;
bool input_alt = HIGH;       
bool led_status = LOW;
unsigned long prell_delay = 100;
unsigned long alte_zeit = 0;

void setup() {
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin); 
    if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
        led_status = !led_status;
        digitalWrite(led_pin, led_status);
        alte_zeit = millis();
  }
  input_alt = input;
}

Wir haben die Variablen input_alt, prell_delay und alte_zeit hinzugefügt. Im loop() ergänzen wir die Bedingungen für das Ausführen der if-Anweisung. Der neue Tasteneingang muss LOW (also an) und der vergangenen Tasteneingang muss HIGH (also aus) gewesen sein. Außerdem soll die Subtraktion aus der aktuellen und der vergangenen Zeit größer, als die vorgegebene Zeitspanne für das Entprellen sein. Dann darf die LED umgeschaltet werden.

Wir dürfen dann nicht vergessen, die aktuelle Zeit als neue Vergleichszeit für den nächsten Durchlauf zu speichern. Außerdem müssen wir dann noch den aktuellen Tasteneingang als alten Tasteneingang speichern, damit wir ihn ebenfalls im nächsten Durchlauf vergleichen können.

Laden wir das Programm auf den Arduino, sollte das Umschalten der LED ordnungsgemäß funktionieren. Eventuell kann die Zeitvorgabe für das Entprellen angepasst werden. Abhängig davon, welchen Taster man verwendet. Rein physikalisch müsste man das Loslassen des Tasters ebenfalls entprellen. In der Regel ist das aber nicht unbedingt notwendig.

Statusmaschine programmieren

Wir können bisher LEDs ohne Blockieren blinken lassen, die Helligkeit verändern und mit dem Taster ohne Prellfehler umschalten. Wir wollen nun mehrere Modi einbringen. Im ersten bzw. nullten Modus ist die LED aus. Im nächsten Modus blinkt die LED und im letzten Modus Pulsiert sie. Wir brauchen den Taster für das Umschalten der Modi und natürlich die LED. Der Schaltplan bleibt wie er ist. Wir ignorieren vorerst das Potentiometer.

Für eine Statusmaschine nutzt man vorzugsweise eine switch-case-Anweisung. Darin wird eine Variable (switch) untersucht und abhängig davon, der case eingestellt. Wir wollen drei Modi schreiben, wie zuvor erwähnt. Dafür schaffen wir uns eine weitere Variable namens state und definieren sie mit 0. Im letzten Quellcode werden wir in der if-Anweisung nicht mehr die LED umschalten, sondern die variable state inkrementieren, also erhöhen. Damit schieben wir die Statusmaschine immer um einen Schritt weiter.

Eine switch-case-Anweisung beginnt mit einem switch und der zu untersuchenden Variable in Klammern. Darauf folgt der Rumpf der Anweisung mit den einzelnen Cases. Außerdem sollte ein Standardfall (default) eingefügt werden. Hier ein kurzes Bespiel:

int state = 0;

switch (state) {

    case 0:  [hier passiert etwas]; break;      
    case 1:  [hier passiert etwas anderes]; break;
    case 2:  [hier passiert noch etwas anderes]; break;
    default: [hier ist der Standardfall, falls die Variable nicht 0, 1 oder 2 ist]; break;

}

So sieht ein switch-case in der Programmiersprache C bzw. C++ normalerweise aus. Es wird abhängig von der Variable state der entsprechende Programmteil ausgeführt. Wichtig ist das break am Ende jedes Cases. Sonst wird der nächste Case ebenfalls ausgeführt. Das soll allerdings normalerweise nicht passieren. Der Arduino läuft bekanntlich in einer Schleife. Die switch-case-Anweisung kann man sich wie eine Weiche vorstellen. Bei jedem Umlauf kann man die Weiche auf ein bestimmtes Gleis umstellen, über das der Zug dann fahren wird.

Wir fügen in unser Programm diese switch-case-Anweisung ein. Außerdem holen wir den Quellcode für das nichtblockierende Blinken, sowie das Pulsieren der LED hinzu. In die if-Anweisung für den entprellten Taster fügen wir das Inkrementieren der state-Variable ein. Um zu testen, ob unsere Statusmaschine funktioniert, nutzen wir den seriellen Monitor. Dafür initialisieren wir im setup() die serielle Schnittstelle und fügen in den Cases der switch-case-Anweisung Bildschirmausgaben ein.

Der Quellcode sieht dann wie folgt aus: 

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;
bool input_alt = HIGH;       
bool led_status = LOW;
unsigned long prell_delay = 100;
unsigned long alte_zeit = 0;
int state = 0;

void setup() {
    Serial.begin(115200);
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin); 
    if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
        state++;
        alte_zeit = millis();
    }
    input_alt = input;

    switch (state) {
        case 0: Serial.println("Case 0"); break;
        case 1: Serial.println("Case 1"); break;
        case 2: Serial.println("Case 2"); break;
        default: Serial.println("Default"); break;  
    }
}

Wir ignorieren vorerst die LED, können die Variablen aber auch drin lassen. Starten wir das Programm nach dem Hochladen und öffnen den seriellen Monitor, sehen wir die Ausgabe „Case 0“. Betätigen wir den Taster, schalten wir durch die Cases 1 und 2. Danach sehen wir „Default“. Das liegt daran, dass wir die Variable state endlos weiter inkrementieren.

Da wir aber nur eine bestimmte Anzahl an Cases haben, müssen wir eine weitere Abfrage ergänzen und die Variable state wieder auf 0 setzen, wenn sie einen größeren Wert als 2 einnimmt. Wir definieren eine weitere Variable MAX_STATES, damit wir für einen späteren Fall nur am Beginn des Quellcodes die Maximalanzahl der Cases ändern müssen, statt nach der entsprechenden Stelle im Quellcode zu suchen.

Der geänderte Quellcode sieht dann folgendermaßen aus: 

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;
bool input_alt = HIGH;       
bool led_status = LOW;
unsigned long prell_delay = 100;
unsigned long alte_zeit = 0;
int state = 0;
int MAX_STATES = 2;

void setup() {
    Serial.begin(115200);
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin); 
    if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
        state++;
        if (state > MAX_STATES) {
            state = 0;
        }
        alte_zeit = millis();
    }
    input_alt = input;

    switch (state) {
        case 0: Serial.println("Case 0"); break;
        case 1: Serial.println("Case 1"); break;
        case 2: Serial.println("Case 2"); break;
        default: Serial.println("Default"); break;  
    }
}

Wir haben die Variable MAX_STATES hinzugefügt und in der if-Abfrage für den Taster nach dem Inkrementieren der state-Variable die maximalen Cases eingegrenzt. Laden wir dieses Programm auf den Arduino und starten den seriellen Monitor, können wir zwischen den Cases 0, 1 und 2 umschalten, ohne in den Default-Case zu rutschen.

Verschiedene Modi für die LED

Wir holen uns nun den Quellcode für das Blinken aus dem optimierten BlinkWithoutDelay-Beispiel und setzen es in den Case 1 ein. Dabei müssen wir darauf achten, dass die Variablennamen für den LED-Pin und LED-Status gleich sind.
Der Quellcode lautet dann folgendermaßen:

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;
bool input_alt = HIGH;       
bool ledState = LOW;
unsigned long prell_delay = 100;
unsigned long alte_zeit = 0;
int state = 0;
int MAX_STATES = 2;

unsigned long previousMillis = 0;
const long interval = 1000;
unsigned long currentMillis = 0;

void setup() {
    Serial.begin(115200);
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin); 
    if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
        state++;
        if (state > MAX_STATES) {
            state = 0;
        }
        alte_zeit = millis();
    }
    input_alt = input;

    currentMillis = millis();
  
    switch (state) {
        case 0: Serial.println("Case 0"); break;
        case 1: { 
            Serial.println("Case 1"); 
            if (currentMillis - previousMillis >= interval) {
                previousMillis = currentMillis;
                ledState = !ledState;
                digitalWrite(led_pin, ledState);
            }
        } break;
        case 2: Serial.println("Case 2"); break;
        default: Serial.println("Default"); break;  
    }
}

Wir haben die Variablen aus dem Blink-Beispiel eingefügt und die Namen etwas angepasst. Außerdem in Case 1 das Umschalten der LED abhängig von der Zeit eingefügt. Die aktuelle Zeit nehmen wir vor der switch-case-Anweisung. Laden wir das Programm auf den Arduino, können wir das Blinken starten und stoppen.

Im seriellen Monitor können wir weiterhin das Umschalten der Cases beobachten. Die LED blinkt nur, wenn die Variable state den Wert 1 einnimmt und sich damit die Statusmaschine in Case 1 befindet. Wir stellen aber fest, dass die LED eingeschaltet bleibt, wenn sie während des Blinkens an ist und wir den Case mit dem Taster ändern. Wir möchten aber, dass die LED ausgeschaltet ist, wenn wir in den Case 0 gelangen. Dafür müssen wir also im entsprechenden Case 0 die LED auch ausschalten. Wir ändern Case 0 wie folgt: 

 

. . .
        case 0: {
            Serial.println("Case 0"); 
            ledState = LOW;
            digitalWrite(led_pin, ledState);
        } break;
. . .

Für die bessere Lesbarkeit wurden geschweifte Klammern hinzugefügt. Ergänzen wir diesen Quellcode und laden das Programm auf den Arduino, können wir in Case 0 die LED wieder ausschalten. Nun verhält es sich aber so, dass das Programm in jedem Durchlauf die LED ausschaltet. Um die CPU ein wenig zu entlasten, werden wir nur einmal ausschalten und in allen weiteren Umläufen die CPU ins „leere“ laufen lassen. Man könnte dafür einen weiteren Case einfügen.

Wir lösen das mit einer Variablen, die wir idle nennen und als Bedingung für eine if-Anweisung in Case 0 einfügen. Zu Beginn hat diese Variable den Wert LOW. Wir fragen in Case 0 ab, ob sie LOW ist. Ist diese Bedingung erfüllt, schalten wir die LED aus und setzen die Variable idle auf HIGH. Beim nächsten Durchlauf wird die LED nicht ausgeschaltet, da die Variable immer noch HIGH ist.

Wenn wir den Case 0 mit einem Tastendruck verlassen und nach dem Durchschalten wieder in Case 0 gelangen, muss dort dann die Variable idle LOW sein, damit die LED einmal ausgeschaltet wird. Wir fügen an der vorherigen Stelle im Quellcode, an der wir den Taster entprellen und die state-Variable auf 0 setzen, das LOW setzen der idle-Variable ein. Denn genau zu dem Zeitpunkt brauchen wir den Zustand LOW für diese Variable, da als Nächstes der Case 0 betreten wird.

Wir ändern also folgende Passagen im Quellcode:

. . . 
// vor dem setup():
bool idle = LOW;

. . . 

// im loop():

    if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
        state++;
        if (state > MAX_STATES) {
            state = 0;
            idle = LOW;
        }
        alte_zeit = millis();
    }
. . .
        case 0: {
            Serial.println("Case 0");
            if (idle == LOW) {
                ledState = LOW;
                digitalWrite(led_pin, ledState);
            idle = HIGH;    
            }
        } break;
. . .

Wir haben die neue Variable idle eingefügt. In dem Teil, in dem der Taster entprellt wird, setzen wir sie auf LOW. In Case 0 fragen wir die Variable ab, ob sie LOW ist. Wenn ja, schalten wir die LED aus und setzen die idle-Variable auf HIGH. Ergänzen wir damit unseren Quellcode und führen das Programm aus, hat sich äußerlich nichts geändert. Wir können aber davon ausgehen, dass nun in Case 0 nicht ständig der Ausgang für die LED angetriggert wird.

Als nächsten Schritt werden wir das Potentiometer nutzen, um die Helligkeit der blinkenden LED zu verändern. Da wir nichtblockierend programmieren, sollte sich die Helligkeit während des Blinkens einstellen lassen. Wir übernehmen den Quellcode aus dem Beispiel mit dem Potentiometer. Dabei bemerken wir, dass wir statt digitalWrite() nun analogWrite() nutzen müssen. Damit steuern wir die Helligkeit per Pulsweitenmodulation. Es ist auch möglich, selbst für kurze Schaltzeiten zu sorgen und mit digitalWrite() die Helligkeit der LED zu regeln. Wir bleiben aber bei unserem Beispiel.
Wir ändern und ergänzen folgende Zeilen in unserem Quellcode:

. . . 
// vor dem setup():
int input_analog = 0;
. . . 

// im loop():
. . .
   // direkt nach dem digitalRead()
    input_analog = analogRead(A0);
    input_analog = map(input_analog, 0, 1023, 0, 255);
. . .
    // vor der switch-case-Anweisung
    currentMillis = millis();
. . .
        case 0: {
            Serial.println("Case 0");
            if (idle == LOW) {
                ledState = LOW;
                analogWrite(led_pin, 0);
            idle = HIGH;      
            }
        } break;
        case 1: {
            Serial.println("Case 1");  
            if (currentMillis - previousMillis >= interval) {
                previousMillis = currentMillis;
                ledState = !ledState;
            }
            analogWrite(led_pin, (int)ledState * input_analog);
        } break;
. . .

Wir fügen die Variable input_analog hinzu. Im loop() fast zu Anfang setzen wir das analogRead() und das Mapping ein. Vor der switch-case-Anweisung speichern wir die aktuelle Zeit. in case 0 und case 1 ändern wir den Ausgang für die LED von digitalWrite() in analogWrite(). Damit die Helligkeit der LED während des Blinkens verändert werden kann, verschieben wir die Zeile für den LED-Ausgang nach außerhalb der if-Abfrage.

Hinzu kommt dann noch, dass wir mit analogWrite() als zweiten Parameter den Duty Cycle mit den Werten 0 bis 255 angeben. Wir schalten die LED also nicht mehr mit LOW oder HIGH. Dementsprechend können wir nicht einfach ledState auf den Ausgang geben. Wir nutzen einen kleinen Trick.

Wir konvertieren den Datentypen von ledState in eine Ganzzahl. LOW und HIGH stehen zwar für 0 und 1. Um wirklich sicherzugehen, führen wir die Konvertierung mit (int) vor der Variablen durch. Ist der Wert LOW, wird er zu 0. Wenn wir das dann mit den Werten des Potentiometers multiplizieren, erhalten wir immer 0. Die LED ist aus. Egal, wie das Potentiometer eingestellt ist.

Wenn dann ledState in HIGH und damit 1 geändert wird, ergibt die Multiplikation mit den Eingangswerten des Potentiometers immer unveränderte Ausgangswerte zwischen 0 und 255 und wir können die LED dimmen, wenn sie eingeschaltet ist. Der gesamte Quellcode sieht nun so aus:

const int led_pin =  3;
int taster_pin = 4;
bool input = HIGH;
bool input_alt = HIGH;       
bool ledState = LOW;
unsigned long prell_delay = 100;
unsigned long alte_zeit = 0;
int state = 0;
int MAX_STATES = 2;
unsigned long previousMillis = 0;
const long interval = 1000;
unsigned long currentMillis = 0;
bool idle = LOW;
int input_analog = 0;

void setup() {
    Serial.begin(115200);
    pinMode(led_pin, OUTPUT);
    pinMode(taster_pin, INPUT_PULLUP);
}

void loop() {
    input = digitalRead(taster_pin);
    input_analog = analogRead(A0);
    input_analog = map(input_analog, 0, 1023, 0, 255);
  
    if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
        state++;
        if (state > MAX_STATES) {
            state = 0;
            idle = LOW;
        }
        alte_zeit = millis();
    }
    input_alt = input;
    currentMillis = millis();
    switch (state) {
    case 0: {
        Serial.println("Case 0");
        if (idle == LOW) {
            ledState = LOW;
            analogWrite(led_pin, 0);
            idle = HIGH;      
        }
    } break;
    case 1: {
        Serial.println("Case 1");  
        if (currentMillis - previousMillis >= interval) {
            previousMillis = currentMillis;
            ledState = !ledState;
        }
        analogWrite(led_pin, (int)ledState * input_analog);
    } break;
    case 2: Serial.println("Case 2"); break;
    default: Serial.println("Default"); break;  
  }
}

Für den dritten Modus wollen wir die LED pulsieren lassen. Mit dem Potentiometer soll die Geschwindigkeit des Pulsierens verändert werden.  Zuerst einmal holen wir uns den früheren Quellcode, mit dem wir die LED pulsieren ließen:

. . .
int helligkeit = 0;
int richtung = 1;  
. . .
analogWrite(ledPin, helligkeit);
helligkeit = helligkeit + richtung;

if (helligkeit > 254 || helligkeit < 1) {
    richtung = richtung * -1;
}

Diesen setzen wir in unseren case 2 ein:

. . .
// vor dem setup()
int helligkeit = 0;
int richtung = 1;
. . .
// im loop()
case 2: {
Serial.println("Case 2");
analogWrite(led_pin, helligkeit);
helligkeit = helligkeit + richtung;
if (helligkeit > 254 || helligkeit < 1) {
richtung = richtung * -1;
}
} break;
. . .

Ohne weitere Änderungen laden wir das Programm auf den Arduino. Schalten wir mit dem Taster den dritten Modus ein, pulsiert die LED relativ schnell. Wir müssen nun eine Pause einfügen, die wir mit dem Potentiometer variieren können.

Da wir in case 1 bereits die Zeitabfrage implementiert hatten, übernehmen wir sie für case 2. Wir setzen das Pulsieren in diese
if-Abfrage. Die Bedingung dafür ist nun aber, dass die abgelaufene Zeit größer sein muss, als der Wert vom Potentiometer. Um den Code wirklich korrekt zu schreiben, müssen wir diesen Wert in den Datentyp long konvertieren, damit wir ihn als Zeitangabe für den Vergleich nutzen können. Theoretisch geht es auch ohne. Wir machen das aber hier der Richtigkeit halber.

Die Änderungen von case 2 sehen dann so aus: 

// im loop()
case 2: {
Serial.println("Case 2");
analogWrite(led_pin, helligkeit);
input_analog = map(input_analog, 0, 255, 1, 20);
if (currentMillis - previousMillis >= (long)input_analog) {
helligkeit = helligkeit + richtung;
if (helligkeit > 254 || helligkeit < 1) {
richtung = richtung * -1;
}
previousMillis = currentMillis;
}
} break;
. . .

Der Wertebereich des Potentiometers reicht von 0 bis 255. Für die Periodendauer der pulsierenden LED wollen wir allerdings nur Werte von 1 bis 20 Millisekunden. Es bleibt Ihnen überlassen, mit den Werten etwas zu experimentieren. Um den Wertebereich anzupassen, nutzen wir wieder die map()-Funktion. Laden wir das Programm auf den Arduino hoch, können wir im dritten Modus die Geschwindigkeit des Pulsierens mit dem Potentiometer verändern.

Damit haben wir unser Ziel erreicht. Aber was kann man damit nun noch alles anstellen? Wir haben quasi ein Multitasking auf einer CPU entwickelt. Wenn Sie nun Sensoren haben für Temperatur, oder eine Wasserstandsanzeige, dann können Sie damit z.B. eine optische Warnung in Form einer blinkenden roten LED erzeugen. Der Mikrocontroller verarbeitet weiter die Sensordaten.

Sie können Roboter entwickeln, deren Antriebe mit der Pulsweitenmodulation gesteuert werden. So können Sie z.B. die Werte von Beschleunigungs- oder Entfernungssensoren verarbeiten und nahezu gleichzeitig die Motoren regeln. Alles mit einer CPU. Ihrer Kreativität sind dabei so gut wie keine Grenzen gesetzt.

Hier nun noch der finale Quellcode ohne die Ausgabe auf dem seriellen Monitor:

const int led_pin = 3;
int taster_pin = 4;
bool input = HIGH;
bool input_alt = HIGH;
bool ledState = LOW;
unsigned long prell_delay = 100;
unsigned long alte_zeit = 0;
int state = 0;
int MAX_STATES = 2;
unsigned long previousMillis = 0;
const long interval = 1000;
unsigned long currentMillis = 0;
bool idle = LOW;
int input_analog = 0;
int helligkeit = 0;
int richtung = 1;
void setup() {
pinMode(led_pin, OUTPUT);
pinMode(taster_pin, INPUT_PULLUP);
}
void loop() {
input = digitalRead(taster_pin);
input_analog = analogRead(A0);
input_analog = map(input_analog, 0, 1023, 0, 255);
if (input == LOW && input_alt == HIGH && millis() - alte_zeit > prell_delay) {
state++;
if (state > MAX_STATES) {
state = 0;
idle = LOW;
}
alte_zeit = millis();
}
input_alt = input;
currentMillis = millis();
switch (state) {
case 0: {
if (idle == LOW) {
ledState = LOW;
analogWrite(led_pin, 0);
idle = HIGH;
}
} break;
case 1: {
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
}
analogWrite(led_pin, (int)ledState * input_analog);
} break;
case 2: {
analogWrite(led_pin, helligkeit);
input_analog = map(input_analog, 0, 255, 1, 20);
if (currentMillis - previousMillis >= (long)input_analog) {
helligkeit = helligkeit + richtung;
if (helligkeit > 254 || helligkeit < 1) {
richtung = richtung * -1;
}
previousMillis = currentMillis;
}
} break;
default: break;
}
}

Ich hoffe, ich konnte Ihnen mit den Tipps etwas helfen.

Viel Spaß beim Basteln.

Andreas Wolter
für AZ-Delivery Blog

Auf vielfachen Wunsch hier der Link zum Download als pdf (gültig bis 31.07.2020)

https://www.dropbox.com/s/l73iv7b9tqervx2/Arduino%20IDE_Teil5.pdf?dl=0

https://www.dropbox.com/s/mwnwrfotz3nyzgx/Arduino%20IDE_all.pdf?dl=0

3 Kommentare

Jürgen Eggers

Jürgen Eggers

Lieber Herr Wolter, ein sehr schöner Beitrag, auch für “DAU´s” geeignet. wenn ich unverschämt sein darf würde ich mir weiterführende Tipp´s und Howto´s wünschen. Ich weiß das da sehr viel Arbeit und Liebe zur Programmierung dahintersteckt, dafür ein ganz großes Dankeschön. Mich würde insbesondere interessieren wie “MCU” Baugruppen seriell angesteuert werden oder verschiedenste LCD Anzeigen (gern auch mit Touchfunktion) zum Leben erweckt werden. Viele Skripte werden veröffentlicht ohne sich mühe zu geben ob der Anwender das auch versteht was er gemacht hat. Das war in Ihrem “Kurs” eindeutig anders, ich schließe mich dem Vor-Kommentar gern an, "Weitere dürfen gern folgen! Sie müßten auch bei meinen Übungen einen Schluckauf bekommen haben weil ich nur Ehrfürchtig/wohlwollend an Sie gedacht habe.

Manuel

Manuel

Sehr tolle Beiträge! Weitere dürfen gerne folgen :-)

Herbert

Herbert

Danke für den tollen Betrag.

Einen Kommentar hinterlassen

Alle Kommentare werden vor der Veröffentlichung moderiert