Gta Robotik Programmierung

Hier ist die Seite der GTA-Robotik/Programmierung am Hans-Erlwein-Gymnasium in Dresden.

/assets/img/HEG.jpg

Auf dieser Seite sammeln wir mal die Themen, Projekte und kleinen Erfolge aus dem Ganztagesangebot - auch mal als Referenz zum Nachlesen.

Inhaltsverzeichnis

Robotik und Mikrocontrollerprogrammierung (Arduino)

Mit dem kleinen Arduino Uno, diversen LEDs und weiteren Bauteilen kann man schon eine Menge über Elektrotechnik und Programmierung lernen. Hier sind ein paar Notizen aus der AG.

Arduino-IDE selbst installieren

Zur Installation der Arduino IDE auf dem eigenen Linux-PC siehe Artikel “Arduino IDE unter Linux einrichten”. Auf den neuen Schul-PCs ist die Arduino-IDE bereits vorinstalliert.

Billige Arduino-Klone aus Fernost (zumeist in den Starterkits enthalten) verwenden den CHP341-Chip für die serielle Kommunikation. Dafür fehlt auf den Schul-PCs derzeit noch der Treiber. Diese Arduino-Klone kann man also nur auf unseren GTA-Linux-Notebooks ansteuern.

Arduino-Simulatoren

Obwohl das Zusammenstecken einer Schaltkreises und Ausprobieren mit echter Hardware natürlich am meisten Spaß macht, kann man viele Sache auch im Simulator online testen. Ich verwende hier die Webseite wokwi.com.

Arduino IDE und ein minimalistisches Programm

In der Arduino IDE wird das folgende minimalistische Programm (dort Sketch genannt) angezeigt und nachfolgend von uns befüllt:

1
2
3
4
5
6
7
8
9
void setup() {
  // Diese Funktion setup() wird nur einmal aufgerufen.
  // Hier kommt die Initialisierung rein.
}

void loop() {
  // Diese Funktion wird kontinuierlich hintereinander aufgerufen.
  // Hier kommt die gesamte Regelungslogik/das Ausführungsprogramm rein.
}

Dabei führt der Mikrocontroller im Hintergrund sowas wie das nachfolgende minimalistische C-Programm aus:

1
2
3
4
5
6
7
8
9
10
11
int main() {

	setup(); // einmal aufrufen

	// unendliche Schleife
	while (true) {
		loop(); // immer wieder neu die loop()-Funktion aufrufen
	}

	return 0;
}

Die Grundlagen

Die Abschnitte in diesem Kapitel beschreiben die absoluten Grundlagen der Mikrokontrollerprogrammierung.

LEDs und Schrittketten

Hier ist ein typisches Anfänger-Beispiel mit delay():

1
2
3
4
5
6
7
8
9
10
11
void setup() {
  // PIN 9 als Spannungsausgang konfigurieren
  pinMode(9, OUTPUT);
}

void loop() {
  digitalWrite(9, HIGH);  // Spannung auf +5V setzen (LED geht an)
  delay(1000);            // eine Sekunde warten
  digitalWrite(9, LOW);   // Spannung auf 0 setzen, Verbindung mit Ground (LED geht aus)
  delay(1000);            // eine Sekunde warten
}

Hier haben wir also eine kleine Schrittkette mit 2 Schritten programmiert:

  • Schritt 1: LED ist an
  • Schritt 2: LED is aus

Erweitern wir das Beispiel nun um eine 2. LED.

Schaltplanfrage: Muss man jede LED mit einem eigenen Widerstand absichern (Bild links), oder kann man sich einen Widerstand sparen (Bild rechts)?

Schaltungsvarianten mit 2 LEDs

Lösung: im Prinzip geht die 2. Schaltung auch, da die Dioden ein “Verkehrtherumfließen” des Stroms verhindern. Wenn man aber beim Einbau die 2. LED aus Versehen verkehrt herum einbaut und dann LED 1 auf HIGH zieht und LED 2 auf LOW, dann fließt ein Kurzschlussstrom durch beide LEDs und brutzelt diese weg (und den Arduino möglicherweise gleich mit). Daher sollte man jede LED mit einem eigenen Widerstand absichern.

Aufgabe: Erweitert das Programm so, dass es 2 LEDs, angeschlossen an PIN 8 und 9 blinken lässt. Dabei sollen die LEDs 1 Sekunde an und 300 ms aus sein.

Lösung: Wokwi-Simulator-Lösung mit Schaltung

Programmierung ohne Wartepausen

Wichtig für komplexere Steuerungsprogramme ist, dass man kontinuierlich Sensorwerte auslesen und entsprechende Behandlungsroutinen ausführen kann. Das geht aber nicht, wenn man - wie in fast allen einfachen Beispielprogrammen - mit der delay() Funktion arbeitet. Während der Wartezeit kann man nämlich nichts anderes anderes machen. Wie soll man hier also 2 LEDs arithmisch blinken lassen, so dass man zeitgleich noch anderes machen kann?

Die Grundidee ist, dass man sich vom Mikrokontroller die aktuelle Zeit (in Millisekunden) geben lässt und nur bei Erreichen/Überschreiten eines bestimmten Zeitpunkts eine Aktion ausführt. Das Programm oben könnte dann so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Variable hält Zeitpunkt, an dem als nächstes was gemacht werden soll
unsigned long nextActiveMillis;

// Variable hält aktuellen Zustand der LEDs (aktuellen Schritt in der Schrittkette)
bool LEDsOn;

void setup() {
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  nextActiveMillis = millis(); // wir beginnen sofort mit der ersten Aktion
  LEDsOn = false; // anfänglich sind die LEDs aus
}

void loop() {
  // prüfen, ob wir aktiv werden müssen
  if (millis() >= nextActiveMillis) {

    // welchen Schritt führen wir als nächstes aus
    if (LEDsOn) {
      // LEDs sind an, schalte sie aus
      digitalWrite(8, LOW);
      digitalWrite(9, LOW);
      LEDsOn = false;
      // warte 300 ms bis zur nächsten Aktion
      nextActiveMillis = millis() + 300;
    }
    else {
      // LEDs sind aus, schalte sie an
      digitalWrite(8, HIGH);
      digitalWrite(9, HIGH);
      LEDsOn = true;
      // warte 1000 ms bis zur nächsten Aktion
      nextActiveMillis = millis() + 1000;
    }
  }

  // hier könnten wir jetzt noch anderes Zeug machen, während wir auf das Eintreten des
  // nächsten Schaltzustandes warten
}

Simulatorbeispiel

Aufgabenstellung: Erzeuge folgendes Blinkmuster!

  1. beide LEDs aus
  2. 300 ms warten
  3. Rote LED an
  4. 200 ms warten
  5. Blaue LED an
  6. 500 ms warten

… und bei 1. wiederholen

Lösungsansatz: Statt der Boolischen Variable mit 2 Zuständen (an/aus) braucht man nun 3 Zustände. Also kann man bspw. eine Integervariable dafür verwenden und so eine Schrittkette mit 3 Stufen umsetzen (0 = keine LED an, 1 = nur Rot an, 2 = Rot und Blau an).

Lösung im Simulator

Man kann statt if-Abfragen auch die Switch-Anweisung verwenden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // welchen Schritt führen wir als nächstes aus
    switch (LEDstep) {
      case 0 : 
        // beide ausschalten
        digitalWrite(8, LOW);
        digitalWrite(9, LOW);
        // warte 300 ms bis zur nächsten Aktion
        nextActiveMillis = millis() + 300;
      break;

      case 1:
        // ROT anschalten, BLAU bleibt aus
        digitalWrite(9, HIGH);
        nextActiveMillis = millis() + 200;
      break;

      case 2:
        // BLAU auch anschalten
        digitalWrite(8, HIGH);
        nextActiveMillis = millis() + 500;
      break;
    }

Das macht es etwas übersichtlicher.

LEDs langsam ein-/ausblenden

Dafür verwendet man die PWM-Pins (Pulse-Width-Modulation). Je nachdem, wie lange der Spannungspegel innerhalb des Duty-Cycle hochgezogen ist, umso höher ist die augenscheinliche Analogspannung am PIN Ausgang. Man setzt diesen mit analogeWrite().

Man muss dafür die mit ~ gekennzeichneten PWM-Pins benutzen. In den folgenden Beispielen verwenden wir deshalb Pins 9 und 10! Siehe auch das PINOUT Diagramm: Arduino Uno Rev3 Pinout

Hier ist das Beispielprogramm zum ein- und ausblenden einer an Pin 9 angeschlossenen LED:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int brightness = 0;  // aktuelle Helligkeit (0..255)
int stepSize = 5;    // Wieviele Helligkeitsschritte pro Anpassung verändern

void loop() {
  // Helligkeit/PWM-Spannung am PIN 9 setzen 
  analogWrite(9, brightness);

  // Helligkeit stufenweise ändern
  brightness = brightness + stepSize;

  // Beim Unterschreiten oder Überschreiten der Grenzen die Richtung tauschen
  if (brightness <= 0 || brightness >= 255) {
    stepSize = -stepSize;
  }
  // 30 Millisekunden warten
  delay(30);
}

Auch hier verhindert der delay() Aufruf andere gleichzeitig stattfindende Aktionen.

LEDFade - 2 LEDs in Loop mit millis()

Im nächsten Schritt sollen 2 LEDs (wie im Kapitel oben) gleichzeitig ein- und ausgeblendet werden. Dazu soll wieder die millis()-Funktion verwendet werden. Man braucht also 2 nextActiveMillis Variablen und auch separate Helligkeitszustände je LED. Die Lösung könnte beispielsweise so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Variable hält Zeitpunkt, an dem als nächstes was gemacht werden soll
unsigned long nextActiveMillis_ROT;
unsigned long nextActiveMillis_BLAU;

// Aktualisierungsinterval in ms
int delay_ROT = 50;
int delay_BLAU = 500;

// aktuelle Helligkeit (0..255)
int brightness_ROT = 0;  
int brightness_BLAU = 0;

// Wieviele Helligkeitsschritte pro Anpassung verändern
int stepSize_ROT = 5;
int stepSize_BLAU = 25; // größere Schritte als bei ROT machen

void setup() {
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);

  // Anfangszustand herstellen: beide aus
  digitalWrite(9, LOW);
  digitalWrite(10, LOW);

  nextActiveMillis_ROT = millis(); // wir beginnen sofort mit der ersten Aktion
  nextActiveMillis_BLAU = millis(); // wir beginnen sofort mit der ersten Aktion
}


void loop() {
  // prüfen, ob wir ROT ändern
  if (millis() >= nextActiveMillis_ROT) {

    // Helligkeit stufenweise ändern
    brightness_ROT = brightness_ROT + stepSize_ROT;
    // auf Wertebereich beschränken
    if (brightness_ROT > 255)
      brightness_ROT = 255;
    if (brightness_ROT < 0)
      brightness_ROT = 0;

    // neue Helligkeit (Spannungswert) setzen
    analogWrite(9, brightness_ROT);

    // Beim Unterschreiten oder Überschreiten der Grenzen die Richtung tauschen
    if (brightness_ROT <= 0 || brightness_ROT >= 255) 
      stepSize_ROT = -stepSize_ROT;

    nextActiveMillis_ROT = millis() + delay_ROT;
  }

  // prüfen, ob wir BLAU ändern
  if (millis() >= nextActiveMillis_BLAU) {

    // Helligkeit stufenweise ändern
    brightness_BLAU = brightness_BLAU + stepSize_BLAU;
    // auf Wertebereich beschränken
    if (brightness_BLAU > 255)
      brightness_BLAU = 255;
    if (brightness_BLAU < 0)
      brightness_BLAU = 0;

    // neue Helligkeit (Spannungswert) setzen
    analogWrite(10, brightness_BLAU);

    // Beim Unterschreiten oder Überschreiten der Grenzen die Richtung tauschen
    if (brightness_BLAU <= 0 || brightness_BLAU >= 255) 
      stepSize_BLAU = -stepSize_BLAU;

    nextActiveMillis_BLAU = millis() + delay_BLAU;
  }

  // hier könnten wir jetzt noch anderes Zeug machen, während wir auf das Eintreten des
  // nächsten Schaltzustandes warten
}

In dieser Variante wird sowohl das Aktualisierungsinterval, z.B. in Variable delay_ROT, definiert als auch die Helligkeitsschritte je Anpassung stepSize_ROT. Dadurch faded die rote LED gleichmäßig ein- und aus und die blaue LED ändert sich sichtbar in diskreten Schritten.

Aufgabe: Schaltung nachbauen und im Programm die Parameter delay_xxx und stepSize_xxx ändern und den Einfluss anschauen.

Beispiel im Simulator

LEDFade - Klassenprogrammierung

Im letzten Beispiel stellen wir fest, dass die Variablen alle irgendwie doppelt da sind. Wenn man mal 10 LEDs ansteuern will, wird das schnell unübersichtlich. Man kann aber Variablen, die irgendwie zu einem Objekt (hier der LED) dazugehören, in einer Klasse gruppieren. Die gesamte Programmlogik des Ein-/Ausblendes einer LED wird nun in eine Klasse verschoben.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Klasse, welche die Funktionalität für das Ein-/Ausblenden einer LED kapselt
class LEDFade {
public:
  void init(int pin, bool direction, int value, int delay, int stepSize) {
    // Membervariablen initialisieren
    m_pin = pin;
    m_direction = direction;
    m_value = value;
    m_delay = delay;
    m_stepSize = stepSize;
    m_nextActiveMillis = millis();

    pinMode(m_pin, OUTPUT); // Als Ausgabe-PIN festlegen
  }

  // Diese Funktion setzt einen neuen Helligkeitswert für die LED
  void setValue(int value) {
    if (value < 0)
      value = 0;
    if (value > 255)
      value = 255;
    m_value = value;
    // Quadratische Funktion verwenden, um gleichmäßig die 
    // Helligkeit der LED zu erhöhen
    // v(x) = x² * 255 / 255² = x²/255
    analogWrite(m_pin, m_value*m_value/255);
  }

  void fade() {
    // Fehlerbehandlung
    if (m_pin == 0)
      return; 

    // LED erst aktualisieren, wenn die geforderte Wartezeit überschritten wurde
    if (millis() < m_nextActiveMillis) 
      return; // not yet there, skip this

    m_nextActiveMillis = millis() + m_delay;
 
    // wenn wir die Helligkeit erhöhen
    if (m_direction) {

      m_value += m_stepSize;
      // am oberen Ende begrenzen und Richtung wechseln
      if (m_value >= 255) {
        m_value = 255;
        m_direction = false; // ab jetzt dunkler werden
      }
      
    }
    else {
      // sonst, wenn wir die Helligkeit reduzieren

      m_value -= m_stepSize;
      // am unteren Ende begrenzen und Richtung wechseln
      if (m_value <= 0) {
        m_value = 0;
        m_direction = true; // ab jetzt heller werden
      }

    }
    // Neuen Helligkeitswert setzen
    setValue(m_value);
  }


private:
  int   m_pin                = 0;        // PIN der LED
  bool  m_direction          = true;     // true -> Helligkeit erhöhen
  int   m_delay              = 50;       // Wartezeit zwischen Anpassungen in ms
  int   m_value              = 0;        // Aktuelle Helligkeit
  int   m_stepSize           = 1;        // Schrittgröße
  unsigned long m_nextActiveMillis = 0;  // Nächster Aktualisierungszeitpunkt
};


// Variablen für die beiden LEDs
LEDFade led1;
LEDFade led2;

void setup() {
  // die Klassenvariablen mit den jeweiligen Parametern initialisieren
  
  // PIN 9: anfänglich aufblenden, Startwert 0, 250 ms Wartezeit, und
  //         25er Schritte machen
  led1.init(9, true, 0, 250, 25);  
  
  // PIN 10 etwas anders konfiguriert
  led2.init(10, false, 255, 30, 5);
}


// die Hauptschleife ist nun sehr schön aufgeräumt und minimalistisch
void loop() {
  led1.fade();
  led2.fade();
}

Um die Helligkeit möglichst linear anzupassen, verwenden wir den im Artikel “LED Helligkeit linear regeln” beschriebenen Trick).

Hier ist das Beispiel im Simulator

Aufgabe: Die ganze Logik rund um nextActiveMillis lässt sich auch in eine eigene Klasse packen. Erstelle eine Klasse TimedAction mit einer Memberfunktion isActive(), welche dann true zurückliefert, wenn man den nächsten aktiven Zeitpunkt erreicht hat. Diese Klasse soll dann von der Klasse LEDFade verwendet werden.

Lösung: Die Klasse TimedAction könnte so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Klasse, welche die "bin ich jetzt dran" Funktion kapselt
class TimedAction {
public:
  void init(int delay) {
    m_delay = delay;
    m_nextActiveMillis = millis();
  }
  
  void reset() {
    m_nextActiveMillis = millis();
  }

  bool isActive() {
    if (millis() < m_nextActiveMillis) 
      return false; // Aktiv-Zeitpunkt noch nicht erreicht

    // nächsten Zeitpunkt festlegen
    m_nextActiveMillis = millis() + m_delay;

    return true; // Jetzt sind wir aktiv
  }

private:
  unsigned long m_nextActiveMillis   = 0;   // Nächster Aktualisierungszeitpunkt
  int           m_delay              = 50;  // Wartezeit zwischen Anpassungen in ms
};

Die Klasse hat drei Member-Funktionen init(delay), reset() und isActive(). Letztere wird in jeder Ausführung von loop() direkt oder indirekt aufgerufen und kümmert sich um das Erkennen des aktiven Moments.

Komplettes Beispiel im Simulator.

Integer Overflow Protection

Wenn der Arduino sehr lange läuft, dann ist irgendwann der 32-bit Datentyp unsigned long nicht mehr groß genug, die Anzahl der Millisekunden seit dem Start zu zählen. Der Maximalwert ist 4.294.967.295, entspricht also ca. 4.294.967 s und damit 49 Tagen und 17 Stunden. Danach läuft die Zahl über und beginnt wieder bei 0. Für die Klasse TimedAction bedeutet das, dass in der Zeile:

1
m_nextActiveMillis = millis() + m_delay;

beispeilsweise bei m_nextActiveMillis = 4294967200 und m_delay = 100 dann m_nextActiveMillis = 4 herauskommt (= 4294967300 - 4294967296, wobei 4294967296 = 2^32 ist).

Daraufhin würde in den darauffolgenden 100 Aufrufen von bool isActive() jeweils der nächste Zeitpunkt als erreicht betrachtet werden und der Test ständig wahr sein. Um dieses Problem zu verhindern, kann man die Klasse anpassen und sich statt des nächsten Zeitpunkts den bisherigen Zeitpunkt merken:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Klasse, welche die "bin ich jetzt dran" Funktion kapselt
class TimedAction {
public:
  void init(int delay) {
    m_delay = delay;
    m_lastActiveMillis = millis();
  }
  
  void reset() {
    m_lastActiveMillis = millis();
  }

  bool isActive() {
    // Schutz vor Integerüberlauf. In diesem Fall wäre millis() < m_lastActiveMillis
    if (millis() < m_lastActiveMillis) {
      m_lastActiveMillis = millis(); // Letzten Aufrufzeitpunkt zurücksetzen
      return;
    }
    
    if (millis() < m_lastActiveMillis + m_delay) 
      return false; // Aktiv-Zeitpunkt noch nicht erreicht

    // letzten Zeitpunkt festlegen
    m_lastActiveMillis = millis();

    return true; // Jetzt sind wir aktiv
  }

private:
  unsigned long m_lastActiveMillis   = 0;   // Letzter Aktualisierungszeitpunkt
  int           m_delay              = 50;  // Wartezeit zwischen Anpassungen in ms
};

Bei dieser Implementierung können zwar immer noch Überläufe auftreten:

  • wenn die Addition m_lastActiveMillis + m_delay überläuft, kommt da ein Wert kleiner als millis() raus, und die Prüfbedingung wird nicht aktiv, ist also nicht kritisch. Irgendwann läuft auch millis() über, und das wird dann wie folgt behandelt.
  • wenn millis() überläuft, bevor m_lastActiveMillis + m_delay erreicht ist, dann wird das mit einer zusätzlichen Prüfung abgefangen und der letzte Zeitpunkt zurückgesetzt. Dieser Fall könnte z.B. wie folgt eintreten: m_nextActiveMillis = 4294967200 und m_delay = 50, also in Summe 4294967250. millis() liefert nun in einem Aufruf von active() 4294967220 zurück. Die Bedingung ist nicht erfüllt, die Funktion kehrt zurück. Soweit, so gut. Nun wird im Arduino-Programm eine andere längere Operation durchgeführt und der nächste Aufruf findet dann erst 100 Millisekunden später statt. Dann ist aber millis() schon übergelaufen und liefert z.B. 80 zurück. Ohne die Prüfbedingung millis() < m_lastActiveMillis würde nun die Funktion active() ewig lange aufgerufen werden, bis dann irgendwann mal millis() > 42949672500 wird.

Fortgeschrittene Themen

Ansteuerung mehrerer LEDs (Output-PINS) und Bitmasken

Aufgabenstellung: Schreibe eine Klasse, welche die Ansteuerung mehrerer LEDs kapselt. Dabei soll der Zustand aller LEDs durch eine einzige Zahl festgelegt werden (Stichwort: Bitmaske).

Beispielverwendung der Klasse:

1
2
3
4
5
6
void loop() {
   leds.set(5); // schalte nur LED 1 und 3 an
   delay(30);
   leds.set(2); // schalte nur LED 2 an
   delay(30);
}

Bitfelder und Bitmasken

Wenn man mehrere boolische AN/AUS Größen verwalten will (bspw. die Zustände verschiedener LEDs) kann man das in C-Arrays definieren, bspw.:

1
2
3
4
5
6
7
8
9
10
const int NUM_LEDS = 10;
bool LEDstates[NUM_LEDS];

// alle LEDS ausschalten
for (int i=0; i<NUM_LEDS; ++i)
  LEDstates[i] = false;
  
// LED 1 und 3 einschalten (ACHTUNG: 0-basierte Nummerierung)
LEDstates[0] = true;
LEDstates[2] = true;

Allerdings braucht man dafür unnötig viel Speicher (mind. 1 Byte pro LED Zustand), und das gleichzeitige Umschalten der Zustände mehrerer LEDs braucht mehrere Zuweisungen. Da man aber den AN/AUS-Zustand bereits mit einem einzelnen Bit definieren kann, reicht eine 8-Bit Variable für 8 unterschiedliche Zustände aus.

Alternativ kann man Bitfelder benutzen.

Einführung Bitfelder und Bitmasken

  • Beispielsweise wird die Dezimalzahl 6 wird als Binärzahl b0110 dargestellt (siehe auch Beispiele im Foto oben). Jedes Bit der Zahl steht für einen der AN/AUS-Zustände.
  • Man kann über logische Operationen nun herausfinden, welches Bit gesetzt ist
  • Eine Bitmaske ist dabei eine Zahl, bei der in der binären Darstellung ganz bestimmte Bits gesetzt sind

Man braucht nun Funktionen um:

  • den Zustand einzelner Bits an-/auszuschalten
  • zu prüfen, ob ein einzelnes oder mehrere Bits angeschaltet sind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Prüfen, ob ein Bit gesetzt ist

int zahl = 5;

int bitmaske_bit_0 = 1;
int bitmaske_bit_1 = 2;
int bitmaske_bit_2 = 4;

if (  (zahl & bitmaske_bit_0) != 0) {
  // bit 0 gesetzt
}

if (  (zahl & bitmaske_bit_1) != 0) {
  // bit 1 gesetzt
}

if (  (zahl & bitmaske_bit_2) != 0) {
  // bit 2 gesetzt
}

Wie kann man Bitmasken mit jeweils nur einem gesetzten Bit an einer bestimmten Stelle generieren?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  void setLEDs(int ledstates) {
    // ledstates = Ein Bitfeld, in dem die Bits der anzuschaltenden LEDs gesetzt sind

    // Alle LEDs durchlaufen und endsprechend ein/ausschalten

    for (int i=0; i<NUM_LEDS; ++i) {
      // Bitmaske für die ite LED über Shift-Operator << generieren
      int bitmask =  (1 << i);  // Bitshift links
      // Testen, ob Bit gesetzt
      bool on = ( (ledstates & bitmask) > 0);
      // LED ein/ausschalten
      digitalWrite( m_pin[i], on);
    }
  }

Hier ist noch ein umfangreicheres Beispiel für das Arbeiten mit Bitfeldern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <iostream>
#include <iomanip>
#include <string>

//  Dez    Binär      Hex
//    0        0        0
//    1        1        1
//    2       10        2
//    3       11        3
//    4      100        4
//    5      101        5
//    6      110        6
//    7      111        7
//    8     1000        8
//    9     1001        9
//   10     1010        A
//   11     1011        B
//   12     1100        C
//   13     1101        D
//   14     1110        E
//   15     1111        F
//   16    10000       10



std::string bitfield(
	unsigned int i,
	unsigned int nBits = 8)
{
	// generate bitfield for number i
	std::string bits(nBits, '0');
	for (unsigned int b = 0; b < nBits; ++b) {
		int bitmask = (1 << b);
		//  0000001
		//  0000001 << 2   -> 0000100
		//  0000100 >> 1   -> 0000010

		// i =  10001001
		//
		//      10001001
		// &    01111000   AND
		//      --------
		//      00001000

		//
		//      10001001
		// |    01111000    OR
		//      --------
		//      11111001

		//
		//      10001001
		// ^    01111000    XOR
		//      --------
		//      11110001

		if ((i & bitmask))
			bits[nBits - b - 1] = '1';
		else
			bits[nBits - b - 1] = '0';
	}
	return bits;
}


int main()
{
	// write table

	std::cout << std::left << std::setw(10) << "Dezimal"
			  << std::left << std::setw(10) << "Binär"
			  << std::left << std::setw(10) << "Hexadezimal" << std::endl;

	for (unsigned int i = 0; i < 128; ++i) {
		std::cout << std::left << std::setw(10) << std::dec << i
				  << std::left << std::setw(10) << bitfield(i)
				  << std::left << std::setw(10) << std::hex << i << std::endl;
	}


	std::cout << "------------" << std::endl;

	// bitshift rotation

	unsigned int d = 664;

	for (unsigned int i=0; i<32; ++i) {

		// was ist das höherwertige Bit
		unsigned int bitmask = (1 << 31);

		bool bitIsSet = d & bitmask;

		// links shift
		d = d << 1;

		//  falls höchstes Bit gesetzt war, wieder als niedrigstes setzen
		if (bitIsSet)
			d = d | 1;

		std::cout << bitfield(d, 32) << std::endl;
	}


	return 0;
}

Mehrere LEDs bequem mit einer Klasse ansteuern

Programmieraufgabe: Für eine bestimmte Anzahl von PINs am Arduino soll nun eine Klasse diese Funktionalität kapseln. Gewünschte Verwendung wie im nachfolgenden Beispiel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Die zu schreibende Klasse LEDstates
class LEDstates {
  // TODO : GTA Teilnehmer
};


// Klassename LEDstates, wir erstellen eine Klassenvariable LEDs
LEDstates LEDs;

const int NUM_PINS = 5;
int PINS[NUM_PINS] = {3, 4, 5, 6, 7};

void setup() {
  // Klassenobjekt LEDs konfigurieren -> Anzahl der PINS/LEDS wird übergeben
  // und der C-Array mit den PIN IDs
  LEDs.setup(NUM_PINS, PINS);
  // alle LEDs ausschalten (quasi wie die Funktion setLEDs() oben programmiert)
  LEDs.set(0);
}


void loop() {
   int permutations = 1 << LEDs.count(); // = 2^NUM_LEDS
   for (int i=0; i<permutations; ++i) {
     LEDs.set(i);
     delay(30);
   }
}

Lösung:: Hier ist eine Beispielimplementierung für die Klasse LEDstates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Die Klasse LEDstates, welche mehrere digitale Pins parallel verwalten und schalten kann
class LEDstates {
public:
  void setup(int count, int pins[]){
    m_count = count;
    m_pins = pins;
    // Pinmode auf OUTPUT stellen
    for (int i = 0; i<count; ++i) {
      pinMode( pins[i], OUTPUT);
    }
  }

  int count() { return m_count; }

  void set(int ledstates) {
    // ledstates = Ein Bitfeld, in dem die Bits der anzuschaltenden LEDs gesetzt sind
    // Alle LEDs durchlaufen und endsprechend ein/ausschalten
    for (int i=0; i<m_count; ++i) {
      // Bitmaske für die i-te LED über Shift-Operator << generieren
      int bitmask =  (1 << i);  // Bitshift links
      // Testen, ob Bit gesetzt
      bool isOn = ( (ledstates & bitmask) > 0);
      // LED ein/ausschalten
      digitalWrite( m_pins[i], isOn);
    }
  }
  
private:
  int m_count = 0;  // Anzahl der PINS
  int *m_pins;      // Zeiger auf Integer-Array mit PIN Nummern
};

Ein komplette Beispiel findet ihr hier im Simulator

Projekte und Projektideen

Die HEG Schaufensterinstallation

Das Projekt nimmt langsam Gestalt an. Folgende Komponenten sind vorgesehen:

  • Helligkeitssensor (damit kann man das Projekt aktivieren, z.B. wenn man mit dem Handy-Flash drauf leuchtet)
  • 3 “Charge” LEDs, die zum Aktivieren der Schaltung nacheinander aufgeblendet werden
  • 8 Blink-LEDs für Anzeigen (“binäres Zählen”, random blinking, Knight-Raider-Modus etc.)
  • LCD 16x2 Display für Laufschrift und Animation

Die aktuelle Code-Version ist hier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// Einbinden unserer Hilfsklassen
#include "LEDstates.h"
#include "LEDFade.h"
#include "TimedAction.h"

class Charge {
public:
  Charge(LEDFade * fade1, LEDFade * fade2, LEDFade * fade3) : 
    m_fade1(fade1),
    m_fade2(fade2),
    m_fade3(fade3)
  {
  }


  void reset() {
    m_timer.reset();
    m_counter = 1; 
    // 1 heißt "Charge hat begonnen" - wenn wir auf 0 
    // zurückfallen, ist Charge-Modus wieder aus
  }


  void updateLEDBrightness() {
    // fade 1 - wenn counter zwischen 0 ... 255
    // fade 2 - wenn counter zwischen 256 ... 511
    // fade 3 - wenn counter zwischen 512 ... 767

    if (m_counter > 255) {
      m_fade1->setValue(255); // komplett an
    }
    else {
      m_fade1->setValue(m_counter);
    }
    if (m_counter > 511) {
      m_fade2->setValue(255); // komplett an
    }
    else {
      int value = m_counter - 256;
      if (value < 0)
        value = 0; // falls counter < 256 ist
      m_fade2->setValue(value);
    }

    int value = m_counter - 512;
    if (value < 0)
      value = 0; // falls counter < 512 ist
    m_fade3->setValue(value);
  }


  void updateCharge(bool lightIsOn) {
    // sind wir dran mit aktualisieren?
    if (!m_timer.isActive())
      return;

    if (lightIsOn) {
      // maxvalue = 3*256 - 1
      if (++m_counter > 767) {
        m_counter = 767;  // Counter bei Maximalwert stoppen
        m_hasReachedMaximum = true;
      }
    }
    else {
      if (--m_counter < 0) {
        m_counter = 0;  // Counter bei Maximalwert stoppen
        m_hasReachedMaximum = true;
      }
    }
    updateLEDBrightness();
  }

  bool isFullyCharged() const {
    return m_hasReachedMaximum && m_counter > 0;
  }

  int m_counter = 0;

private:
  TimedAction   m_timer;
  // Zeiger auf fade-Klassenobjekte (werden extern erzeugt und hier nur benutzt)
  LEDFade      *m_fade1; 
  LEDFade      *m_fade2;
  LEDFade      *m_fade3;
  bool          m_hasReachedMaximum = false;
};



// Zustände unserer Steuerung
enum {
  S_IDLE,    // In diesem Modus wird nur eine LED gefaded
  S_CHARGE,  // In diesem Zustand werden die Charge-LEDs auf-/abgeblendet
  S_ON_MODE1 // Schaltung läuft im Modus 1 los (Willkommen im LCD Display)

  // TODO : weitere Zustände hinzubauen
};


// ** KONSTANTEN/PIN MAPPING **

// Inputs
int BRIGHTNESS_PIN = A0;

// Outputs

// PWM Pins
int CHARGE_PIN1 = 9;
int CHARGE_PIN2 = 10;
int CHARGE_PIN3 = 11;

// Pins für die LEDs
int LED_PINS[8] = { 2, 4, 7, A1, A2, A3, A4, A5};

// ** VARIABLEN **

// Zur Ansteuerung der dimmbaren Charge-LEDs
LEDFade fade1;
LEDFade fade2;
LEDFade fade3;

// Controller für die ganze Charge-Logik
Charge charge(&fade1, &fade2, &fade3);

// Klasse für die 8 LEDs
LEDstates leds;

// Aktueller Zustand der Schaltung
int state = S_IDLE;

// Variablen für verschiedene Modi
TimedAction onModeTimer;
int onModeCounter;
int ledNumberCounter = 0;

// Timer für Debug-Ausgaben durch die serielle Schnittstelle
TimedAction serialTimer;


void setup() {
  fade1.init(CHARGE_PIN1, true, 0, 25, 5); // Konfiguriert zum langsam pulsieren
  fade2.init(CHARGE_PIN2, true, 0, 10, 5);
  fade3.init(CHARGE_PIN3, true, 0, 10, 5);

  pinMode(BRIGHTNESS_PIN, INPUT);

  leds.setup(8, LED_PINS);
  leds.set(0);

  serialTimer.init(500); // alle 500 ms eine Meldung über USB
  Serial.begin(115200);
}



// ** Hauptschleife, wird ständig durchlaufen **
void loop() {

  // Den Helligkeitssensor lesen wir immer aus.
  int brightnessValue = analogRead(BRIGHTNESS_PIN);
  // wenn Helligkeit größer als 900, dann ist das Licht an
  bool lightIsOn = (brightnessValue > 900); 

  // Je nachdem in welchem Modus die Schaltung ist, machen wir verschiedene Sachen.
  switch (state) {
    case S_IDLE: // Rote LED pulsieren lassen und auf Licht-An warten
      fade1.fade();
      if (lightIsOn) {
        state = S_CHARGE; // Licht ist an - wir wechseln in den Zustand 'Charge'
        charge.reset();
      }
    break;

    case S_CHARGE: // Charge-LEDs werden heller (oder ohne Licht wieder dunkel)
      charge.updateCharge(lightIsOn);
      // sind wir zurück auf 0 gefallen, gehen wir wieder in den Modus IDLE
      if (charge.m_counter == 0)
        state = S_IDLE;
      // sind wir vollkommen aufgeladen, gehen wir in den Modus ON_MODE1
      if (charge.isFullyCharged()) {
        state = S_ON_MODE1;
        onModeCounter = 0; // wir beginnen mit LED-Stufe 0
        onModeTimer.init(100);
      }
    break;

    case S_ON_MODE1: // in diesem Modus schalten wir alle LEDs schnell 
                     // hintereinander ein und aus und dann das LCD-Display an
      if (onModeTimer.isActive()) {
        ++onModeCounter;
        if (onModeCounter % 2) {  // ungerade Zahl?
          leds.set(255);
        }
        else {
          leds.set(0);
        }
        if (onModeCounter == 20) {
          // for now fall back to idle mode
          state = S_IDLE;
        }
      }
    break;
  }

  // Debug-Feedback
  if (serialTimer.isActive()) {
    Serial.print(brightnessValue);
    Serial.print(",");
    Serial.print(state);
    Serial.print(",");
    Serial.println(charge.m_counter);
  }
}

Die in den vorangehenden Kapiteln vorgestellten Klassen LEDFade, LEDstates und TimedAction sind als Include-Dateien eingebunden. Diese Dateien sind im Quelltextarchiv enthalten:

Mareks Transistorschaltung

  • mittels 2 Output-Pins können insgesamt 4 Zustände geschaltet werden
  • die Transistorschaltung sorgt dafür, dass die jeweils ausgewählte LED angeschaltet wird
  • so kann man 4 LEDs mit nur 2 PINs schalten

Mareks TransistorSchaltung (PIN-Extender)

Es gibt ein Modul, welches als Pin-Extender funktioniert, erklärt in diesem PCF8575 Tutorial.

Projekt: Digitale Sanduhr - 3D Drucker + LED Matrix

Anleitung/Beschreibung zum Projekt LED Sanduhr gibt’s auf https://www.instructables.com/LED-HOURGLASS-USING-ARDUINO

LED Sanduhr

3D Programmierung mit OpenGL

Shader und Speicherbereiche

In diesem Beispiel werden Vertex Array Objects (VAO), Vertex Buffer Objects (VBO) verwendet, um eine Fläche zu zeichnen.

Rotating Cube

Index-Puffer

Erweiterung des “rotierenden Würfel”-Beispiels um einen Index-Puffer. Nun werden nur noch die Koordinaten der Eckpunkte im Vertexpuffer gehalten, und die 6 Seitenflächen via Elementpuffer/Indexpuffer gezeichnet.

Rotating Cube - Index Buffer

Normalenvektoren und Phong Shader

Erweiterung um Normalenvektoren - wird für die Lichtreflektionsberechnung benutzt.