In der ersten Aufgabe werden die Grundlagen für Folgeprojekte mit dem Fridolin gelegt.
Da der Fridolin ein Fahrerloses Fahrzeug ist muss alles automatisiert werden. Im ersten Schritt werden drei elementare Funktionen realisiert:
- Anzeigen der aktuellen Aufgabe auf dem Display und auf dem seriellen Monitor am Rechner
- Bedienen des Fridolin mit den drei Tastern, um die Zustände in 3 zu wählen
- 2 einfache Daueraufgaben: LED an - aus oder blinken - aus (Simulation mit Factorio)
Damit wird die unterste Stufe der Automatisierung erreicht: Bedienen und Beobachten! Ich erläutere hier meine Lösung. Eine ganz andere Lösung hat Meister Edel, der den Fridolin gebaut hat, geschrieben. Diese steht hier zum Download bereit: Erste-Lösung-vom-Meister
It works! Beiden Lösung ist gemeinsam, dass delay() nicht genutzt wird. Das wird in vielen Arduino Seiten diskutiert. Am besten mit Google "arduino mulittasking" suchen. Meine Lösung ist deutlich komplizierter als die vom Meister Edel.
Vorüberlegungen
Skizze meiner Programmstruktur |
- DATEI new.try
Versionsdokumentation, Definitionen, Verwaltung bedingte Compilierung, setup(), loop() und
die entscheidende Hauptroutine: LED_control() - DATEI initialisierung
Konfigurationen und Kalibrieren, falls notwendig, sowie Testen aller Komponenten
wird alles in setup() aufgerufen. - DATEI basicIO
low level Routinen, die direkt mit der Hardware kommunizieren. Alle andern Teile rufen die auf. - DATEI TaskManagement
hier sammle ich alle Aufgaben, die "gleichzeitig" ablaufen sollen. Momentan nur Blinken der LED.
Hier ist der Stand vom 04.12.2021: NewTry_v1.1.1
Taster Auslesen
Die drei Taster werden simplem mit Digital Read ausgelesen. Also simpel auf HIGH und LOW abfragen. Aber was mache ich mit Taster 3? Mal soll er einschalten mal Ausschalten. Meister Edel hat das so gelösst:
/------------------------ T3 Abfragen ----------------------------
/------------------------ T3 Abfragen ----------------------------
T3_a = digitalRead(T3); // Taster 3 abfragen und Zustand auf Variable übertragen
if(T3_z == 0 && T3_a == LOW) // Fallende Flanke auswerten
Er merkt sich den Zustand des Tasters 3 im letzten Zyklus und schaut nach ob er gedrückt wurde. [2] Das habe ich anders gemacht. Ich habe dafür eine kleine Routine, die jedes mal alle Taster fragt und sich das merkt:
if(T3_z == 0 && T3_a == LOW) // Fallende Flanke auswerten
Er merkt sich den Zustand des Tasters 3 im letzten Zyklus und schaut nach ob er gedrückt wurde. [2] Das habe ich anders gemacht. Ich habe dafür eine kleine Routine, die jedes mal alle Taster fragt und sich das merkt:
/*
* Taster[0][0] = T1 0->1 Taster[1][0] = T1 down Taster[2][0] = T1 1->0
* Taster[0][1] = T2 0->1 Taster[1][1] = T2 down Taster[2][1] = T1 1->0
* Taster[0][2] = T3 0->1 Taster[1][2] = T3 down Taster[2][2] = T1 1->0
*/
/*
int ReadTaster(){
for (int i=0;i<=2;i++){
Taster[0][i]=false; // Negative Flanke aus
Taster[2][i]=false; // Positive Flanke aus
if(digitalRead(TasterPin[i])==LOW) {
if(!Taster[1][i]){Taster[2][i]=true;} // Taster[2][i] = Ti 1->0
}
else{
if(Taster[1][i]){Taster[0][i]=true;} // Taster[0][i] = Ti 0->1
}
Taster[1][i]=!digitalRead(TasterPin[i]); // Taster[1][i] = Ti down
}
}
*/
Sieht erst mal kompliziert aus, funktioniert aber. Aber danach sin in dem kleinen Array Taster[3][3] alle Möglichkeiten als Wahrheitswert gespeichert. Das kann ich dann irgendwo aufrufen, wo ich das grad mal brauche.* Taster[0][0] = T1 0->1 Taster[1][0] = T1 down Taster[2][0] = T1 1->0
* Taster[0][1] = T2 0->1 Taster[1][1] = T2 down Taster[2][1] = T1 1->0
* Taster[0][2] = T3 0->1 Taster[1][2] = T3 down Taster[2][2] = T1 1->0
*/
/*
int ReadTaster(){
for (int i=0;i<=2;i++){
Taster[0][i]=false; // Negative Flanke aus
Taster[2][i]=false; // Positive Flanke aus
if(digitalRead(TasterPin[i])==LOW) {
if(!Taster[1][i]){Taster[2][i]=true;} // Taster[2][i] = Ti 1->0
}
else{
if(Taster[1][i]){Taster[0][i]=true;} // Taster[0][i] = Ti 0->1
}
Taster[1][i]=!digitalRead(TasterPin[i]); // Taster[1][i] = Ti down
}
}
*/
Ausgabe auf dem Display und serial Monitor
Beim Display einfach nur den Cursor auf die richtige Position setzen und ausgeben. Dafür habe ich ein Mako definiert. Der Compiler setzt automatisch den gesamten Text ein, wenn ich SHOW hinschreibe.
Das regele ich über bedingte Compilierung. D.h. ich sage dem Compiler ob ich den serial Monitor haben will. Das regelt der Text trace: #define trace Wenn mein Programm so läuft, wie ich es haben will. kommt trace weg und voila der serial Monitor ist raus.
#define SHOW(x,y,s) lcd.setCursor (x,y); lcd.print(s);
Das habe ich auch für den Serial Montor gemacht. Da gibt es dann die Variante ohne Zeilenvorschub SAX und mit Zeilenvorschub SAYln. #define SAY(S) (Serial.print(S))
#define SAYln(S) (Serial.println(S))
Natürlich könnte ich das auch immer in den Code schreiben. Meins ist kürzer und habe auch noch einen anderen Vorteil.
Der Fridolin wird ja selbstständig fahren. Dann ist er nicht mehr mit dem Rechner verbunden und kann auch nichts mehr ausgeben. Dann ist der Code Serial.print(S) nuztzlos und kostet nur Zeit. Besser wäre ein Variante, bei der das gar nicht läuft.
#define SAYln(S) (Serial.println(S))
Natürlich könnte ich das auch immer in den Code schreiben. Meins ist kürzer und habe auch noch einen anderen Vorteil.
Der Fridolin wird ja selbstständig fahren. Dann ist er nicht mehr mit dem Rechner verbunden und kann auch nichts mehr ausgeben. Dann ist der Code Serial.print(S) nuztzlos und kostet nur Zeit. Besser wäre ein Variante, bei der das gar nicht läuft.
Das regele ich über bedingte Compilierung. D.h. ich sage dem Compiler ob ich den serial Monitor haben will. Das regelt der Text trace: #define trace Wenn mein Programm so läuft, wie ich es haben will. kommt trace weg und voila der serial Monitor ist raus.
ifdef trace
#define SAY(S) (Serial.print(S))
#define SAYln(S) (Serial.println(S))
#else
#define SAY(S) // ersetzt SAY(S) durch nix
#define SAYln(S) // ersetzt SAY(S) durch nix
#endif
Nebenbei benutze ich SAYln, um auf dem Rechner das Programmablauf zu beobachten. Bei jeder Routine füge ich am Angang SAYln("NameDerRoutine ....") und am Ende SAYln("NameDerRoutine DONE"). Dann kann ich beobachten ob alles so läuft wie geplant. #define SAY(S) (Serial.print(S))
#define SAYln(S) (Serial.println(S))
#else
#define SAY(S) // ersetzt SAY(S) durch nix
#define SAYln(S) // ersetzt SAY(S) durch nix
#endif
Hauptprogramm
Weil ich viele Funktionen in andere Dateien ausgelagert habe ist das recht kurz.
Das UnterProgramm LED_blink ist ein kleine Routine, aus der Datei Taskmanagement:
/*
* Implementiert eine Zustandsmaschine
* active 0: LED ist aus
* active 1: LED ist aktiv je nach Modus
* Modus 0: LED ist Dauericht
* Modus 1: LED is im Blinkmodus
*/
byte Modus=0;
bool active=false; // Funktion an oder aus
void LED_control() {
SAYln("LED_control ....");
if (!active){
SAYln("LED aus");
showfast(0,0," LED aus ");
digitalWrite(LED_BUILTIN,0);
StartTask(nextWakeUpLED); // Timer für LED setzen
}
else {
if (Modus==0){
SAYln("LED an");
showfast(0,0," LED an ");
digitalWrite(LED_BUILTIN,1);
}
else {
SAYln("LED blinkt");
showfast(0,0," LED blinkt");
LED_blink(); // Default = blitzt kurz auf = Programm läuft
}
}
SAYln("LED_control done");
}
* Implementiert eine Zustandsmaschine
* active 0: LED ist aus
* active 1: LED ist aktiv je nach Modus
* Modus 0: LED ist Dauericht
* Modus 1: LED is im Blinkmodus
*/
byte Modus=0;
bool active=false; // Funktion an oder aus
void LED_control() {
SAYln("LED_control ....");
if (!active){
SAYln("LED aus");
showfast(0,0," LED aus ");
digitalWrite(LED_BUILTIN,0);
StartTask(nextWakeUpLED); // Timer für LED setzen
}
else {
if (Modus==0){
SAYln("LED an");
showfast(0,0," LED an ");
digitalWrite(LED_BUILTIN,1);
}
else {
SAYln("LED blinkt");
showfast(0,0," LED blinkt");
LED_blink(); // Default = blitzt kurz auf = Programm läuft
}
}
SAYln("LED_control done");
}
void loop() {
currentTime = millis(); // Am Anfang jeden Durchlaufes die Zeit retten
ReadTaster(); // In j-e-d-e-m Zylkus alle Taster aufrufen
// aus den Tastern den neuen Zustand ermitteln
if((Taster[0]&3)==2) {active=true; }
if((Taster[1]&3)==2) {active=false; }
if((Taster[2]&3)==2) {active=!active;}
if(((Taster[0]&3)==2) & ((Taster[1]&3)==3)) {Modus=1;}
if(((Taster[0]&3)==2) & ((Taster[2]&3)==3)) {Modus=0;}
// die LED Routine aufrufen
LED_control();
}
currentTime = millis(); // Am Anfang jeden Durchlaufes die Zeit retten
ReadTaster(); // In j-e-d-e-m Zylkus alle Taster aufrufen
// aus den Tastern den neuen Zustand ermitteln
if((Taster[0]&3)==2) {active=true; }
if((Taster[1]&3)==2) {active=false; }
if((Taster[2]&3)==2) {active=!active;}
if(((Taster[0]&3)==2) & ((Taster[1]&3)==3)) {Modus=1;}
if(((Taster[0]&3)==2) & ((Taster[2]&3)==3)) {Modus=0;}
// die LED Routine aufrufen
LED_control();
}
Das UnterProgramm LED_blink ist ein kleine Routine, aus der Datei Taskmanagement:
/*
* LED blinken lassen als Multitasking-Test
*
* Meldungen
* return= 102: LED-Toggle
* return=-102: Sheduler zu früh
*/
int LED_blink(){
if(currentTime<=nextWakeUpLED) {
return -102;
} else {
if (digitalRead(LED_BUILTIN)) {
nextWakeUpLED=nextWakeUpLED+waitLEDoff;
}
else{
nextWakeUpLED=nextWakeUpLED+waitLEDon;
}
toggle_LED();
return 102;
}
}
Das hat Meister Edel im Prinzip genau so gelöst. * LED blinken lassen als Multitasking-Test
*
* Meldungen
* return= 102: LED-Toggle
* return=-102: Sheduler zu früh
*/
int LED_blink(){
if(currentTime<=nextWakeUpLED) {
return -102;
} else {
if (digitalRead(LED_BUILTIN)) {
nextWakeUpLED=nextWakeUpLED+waitLEDoff;
}
else{
nextWakeUpLED=nextWakeUpLED+waitLEDon;
}
toggle_LED();
return 102;
}
}
Optimierungen
Die Tasterroutine ist gar nicht optimal. Um 9 Wahrheitswerte zu speichern werden 9 Bytes verschwendet. Und dann weis ich nur die letzten beiden Zustände. In der kleineren und schnelleren Version bekommt jeder Taster ein Byte und speichert die letzten 8 Zustände in den 8 Bits.
In jedem Durchlauf wird der Zustand und die Position der Taster auf dem LCD-Display ausgegeben. Aber die Ausgabe auf dem dem LCD-Display ist echt langsam. Damit wird das ganze Programm langsam; aber es wird meistens dasselbe ausgegeben.
Ich habe eine kleine Routine zwischengeschaltet, die sich merkt, was angezeigt wird. Bei jeder Ausgabe wird erst nachgesehen, ob sich was ändert. Ändert sich nichts, dann wird auch nichts aufs Display ausgegeben. Schön schnell!
Aber es geht doch auch ohne die Optimierung! Guter Einwand!
Taster und Display sind für Menschen; die sind nun mal langsam. Aber die Routinen werden permanent sehr schnell aufgerufen und kosten so viel Rechenzeit. Das ist Verschwendung!
In jedem Durchlauf wird der Zustand und die Position der Taster auf dem LCD-Display ausgegeben. Aber die Ausgabe auf dem dem LCD-Display ist echt langsam. Damit wird das ganze Programm langsam; aber es wird meistens dasselbe ausgegeben.
Ich habe eine kleine Routine zwischengeschaltet, die sich merkt, was angezeigt wird. Bei jeder Ausgabe wird erst nachgesehen, ob sich was ändert. Ändert sich nichts, dann wird auch nichts aufs Display ausgegeben. Schön schnell!
Aber es geht doch auch ohne die Optimierung! Guter Einwand!
Taster und Display sind für Menschen; die sind nun mal langsam. Aber die Routinen werden permanent sehr schnell aufgerufen und kosten so viel Rechenzeit. Das ist Verschwendung!
Merke: Der wahre Logistiker hasst Verschwendung jeglicher Art!
Update 9.12.2021
Ich habe den Code ein wenig aufgeräumt und das Tracing im Serial Monitor mit einem Parameter versehen. Damit wird eingestellt wie geschwätzig das Programm ist. Die Version 1.1.2 kannst Du hier runterladen. Doku wie immer in den Kommentaren.
Quellen
- Plum U (2004), "Eine systemtheoretische Betrachtung der Produktentwicklung". Thesis at: Technischen Universität München - Fakultät für Maschinenwesen. München, June, 2004. [BibTeX] [URL]
- Das geht hier, zu Glück. Im allgemeinen ist das nicht so einfach. Es sind mechanische Kontakte, die ein wenig "hüpfen", wenn sie geschlossen werden. Das nennt man Prellen. Wäre das bei unseren Tastern der Fall, müsste das noch abgefangen werden.
Kommentare
Kommentar veröffentlichen